Do not allow NPE if CarrierConfigManager service is null. am: 5cf46dc254 am: de4d24b5a7
Original change: https://android-review.googlesource.com/c/platform/packages/services/Telecomm/+/3122851
Change-Id: Id46b66db4e19605114267d24cdb980436feff934
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 68a8e39..94654a6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11,6 +11,14 @@
out: ["com/android/server/telecom/TelecomStatsLog.java"],
}
+filegroup {
+ name: "telecom-shell-commands-src",
+ srcs: [
+ "src/com/android/server/telecom/TelecomShellCommand.java",
+ ],
+ path: "src",
+}
+
android_library {
name: "TelecomLib",
manifest: "AndroidManifestLib.xml",
@@ -33,7 +41,6 @@
platform_apis: true,
}
-
// Build the Telecom service.
android_app {
name: "Telecom",
diff --git a/flags/Android.bp b/flags/Android.bp
index b089796..d60a5f5 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -39,6 +39,8 @@
"telecom_bluetoothroutemanager_flags.aconfig",
"telecom_work_profile_flags.aconfig",
"telecom_connection_service_wrapper_flags.aconfig",
+ "telecom_remote_connection_service.aconfig",
"telecom_profile_user_flags.aconfig",
+ "telecom_bluetoothdevicemanager_flags.aconfig",
],
}
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 6879d86..296b300 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=tjstuart TARGET=24Q3
flag {
name: "gen_anom_report_on_focus_timeout"
namespace: "telecom"
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index c0f4cba..75efdfa 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -1,58 +1,74 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=grantmenke TARGET=24Q3
flag {
name: "voip_app_actions_support"
+ is_exported: true
namespace: "telecom"
description: "When set, Telecom support for additional VOIP application actions is active."
bug: "296934278"
}
+# OWNER=grantmenke TARGET=24Q3
flag {
name: "call_details_id_changes"
+ is_exported: true
namespace: "telecom"
description: "When set, call details/extras id updates to Telecom APIs for Android V are active."
bug: "301713560"
}
-flag{
+# OWNER=kunduz TARGET=24Q2
+flag {
name: "add_call_uri_for_missed_calls"
+ is_exported: true
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"
}
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
name: "set_mute_state"
+ is_exported: true
namespace: "telecom"
description: "transactional calls need the ability to mute the call audio input"
bug: "310669304"
}
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
name: "get_registered_phone_accounts"
+ is_exported: true
namespace: "telecom"
description: "When set, self-managed clients can get their own phone accounts"
bug: "317132586"
}
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
name: "transactional_video_state"
+ is_exported: true
namespace: "telecom"
description: "when set, clients using transactional implementations will be able to set & get the video state"
bug: "311265260"
}
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
name: "business_call_composer"
+ is_exported: true
namespace: "telecom"
description: "Enables enriched calling features (e.g. Business name will show for a call)"
bug: "311688497"
is_exported: true
}
-flag{
+# OWNER=tgunn TARGET=25Q3
+flag {
name: "get_last_known_cell_identity"
+ is_exported: true
namespace: "telecom"
description: "Formalizes the getLastKnownCellIdentity API that Telecom reliees on as a system api"
bug: "327454165"
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
new file mode 100644
index 0000000..4c91491
--- /dev/null
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q4
+flag {
+ name: "postpone_register_to_leaudio"
+ namespace: "telecom"
+ description: "Fix for Log.wtf in the BinderProxy"
+ bug: "333417369"
+}
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
index 1df1e9b..dc69dd5 100644
--- a/flags/telecom_bluetoothroutemanager_flags.aconfig
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -1,10 +1,10 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=tgunn TARGET=24Q3
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
index de8dd27..8314376 100644
--- a/flags/telecom_broadcast_flags.aconfig
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=tgunn TARGET=24Q3
flag {
name: "is_new_outgoing_call_broadcast_unblocking"
namespace: "telecom"
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
index 72f9db3..d80cfa3 100644
--- a/flags/telecom_call_filtering_flags.aconfig
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=qingzhong TARGET=24Q2
flag {
name: "skip_filter_phone_account_perform_dnd_filter"
namespace: "telecom"
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 27a4b22..ed75f14 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -1,9 +1,28 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=tjstuart TARGET=24Q3
flag {
name: "transactional_cs_verifier"
namespace: "telecom"
description: "verify connection service callbacks via a transaction"
bug: "309541257"
-}
\ No newline at end of file
+}
+
+flag {
+ name: "cache_call_audio_callbacks"
+ namespace: "telecom"
+ description: "cache call audio callbacks if the service is not available and execute when set"
+ bug: "321369729"
+}
+
+# OWNER = breadley TARGET=24Q3
+flag {
+ name: "cancel_removal_on_emergency_redial"
+ namespace: "telecom"
+ description: "When redialing an emergency call on another connection service, ensure any pending removal operation is cancelled"
+ bug: "341157874"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
index 1d81535..63761ec 100644
--- a/flags/telecom_callaudiomodestatemachine_flags.aconfig
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "set_audio_mode_before_abandon_focus"
namespace: "telecom"
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index f5da045..33bccba 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=kunduz TARGET=24Q2
flag {
name: "available_routes_never_updated_after_set_system_audio_state"
namespace: "telecom"
@@ -8,6 +9,7 @@
bug: "292599751"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "use_refactored_audio_route_switching"
namespace: "telecom"
@@ -15,6 +17,7 @@
bug: "306395598"
}
+# OWNER=tgunn TARGET=24Q3
flag {
name: "ensure_audio_mode_updates_on_foreground_call_change"
namespace: "telecom"
@@ -22,6 +25,7 @@
bug: "289861657"
}
+# OWNER=pmadapurmath TARGET=24Q1
flag {
name: "ignore_auto_route_to_watch_device"
namespace: "telecom"
@@ -29,6 +33,7 @@
bug: "294378768"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "transit_route_before_audio_disconnect_bt"
namespace: "telecom"
@@ -36,6 +41,7 @@
bug: "306113816"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "call_audio_communication_device_refactor"
namespace: "telecom"
@@ -43,6 +49,7 @@
bug: "308968392"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "communication_device_protected_by_lock"
namespace: "telecom"
@@ -50,6 +57,7 @@
bug: "303001133"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "reset_mute_when_entering_quiescent_bt_route"
namespace: "telecom"
@@ -57,6 +65,7 @@
bug: "311313250"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "update_route_mask_when_bt_connected"
namespace: "telecom"
@@ -64,9 +73,29 @@
bug: "301695370"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "clear_communication_device_after_audio_ops_complete"
namespace: "telecom"
description: "Clear the requested communication device after the audio operations are completed."
bug: "315865533"
}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "resolve_switching_bt_devices_computation"
+ namespace: "telecom"
+ description: "Update switching bt devices based on arbitrary device chosen if no device is specified."
+ bug: "333751408"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "early_update_internal_call_audio_state"
+ namespace: "telecom"
+ description: "Update internal call audio state before sending updated state to ICS"
+ bug: "335538831"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
index 593b7e5..c0eebf1 100644
--- a/flags/telecom_calllog_flags.aconfig
+++ b/flags/telecom_calllog_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=qingzhong TARGET=24Q2
flag {
name: "telecom_log_external_wearable_calls"
namespace: "telecom"
@@ -8,6 +9,7 @@
bug: "292600751"
}
+# OWNER=ranamouawi TARGET=24Q2
flag {
name: "telecom_skip_log_based_on_extra"
namespace: "telecom"
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index de17eee..f46e844 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "use_improved_listener_order"
namespace: "telecom"
@@ -8,6 +9,7 @@
bug: "24244713"
}
+# OWNER=tjstuart TARGET=24Q3
flag {
name: "fix_audio_flicker_for_outgoing_calls"
namespace: "telecom"
@@ -15,9 +17,21 @@
bug: "309540769"
}
+# OWNER=breadley TARGET=24Q3
flag {
name: "enable_call_sequencing"
namespace: "telecom"
description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
- bug: "297446980"
+ bug: "327038818"
+}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+ name: "transactional_hold_disconnects_unholdable"
+ namespace: "telecom"
+ description: "Disconnect ongoing unholdable calls for CallControlCallbacks"
+ bug: "340621152"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
index 80a8dfe..38e5e13 100644
--- a/flags/telecom_connection_service_wrapper_flags.aconfig
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=grantmenke TARGET=24Q2
flag {
name: "updated_rcs_call_count_tracking"
namespace: "telecom"
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
index e6badde..161b674 100644
--- a/flags/telecom_default_phone_account_flags.aconfig
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=tjstuart TARGET=24Q3
flag {
name: "only_update_telephony_on_valid_sub_ids"
namespace: "telecom"
@@ -8,6 +9,7 @@
bug: "234846282"
}
+# OWNER=tjstuart TARGET=24Q3
flag {
name: "telephony_has_default_but_telecom_does_not"
namespace: "telecom"
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index 08a82ba..ea842ac 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=qingzhong TARGET=24Q2
flag {
name: "early_binding_to_incall_service"
namespace: "telecom"
@@ -8,6 +9,7 @@
bug: "282113261"
}
+# OWNER=pmadapurmath TARGET=24Q2
flag {
name: "ecc_keyguard"
namespace: "telecom"
@@ -15,6 +17,7 @@
bug: "306582821"
}
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "separately_bind_to_bt_incall_service"
namespace: "telecom"
diff --git a/flags/telecom_profile_user_flags.aconfig b/flags/telecom_profile_user_flags.aconfig
index c046de8..feee07d 100644
--- a/flags/telecom_profile_user_flags.aconfig
+++ b/flags/telecom_profile_user_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=huiwang TARGET=24Q3
flag {
name: "profile_user_support"
namespace: "telecom"
diff --git a/flags/telecom_remote_connection_service.aconfig b/flags/telecom_remote_connection_service.aconfig
new file mode 100644
index 0000000..a30f0b2
--- /dev/null
+++ b/flags/telecom_remote_connection_service.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "set_remote_connection_call_id"
+ namespace: "telecom"
+ description: "Sets the telecom call id for remote connections/ conferences."
+ bug: "320242200"
+}
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index 674a968..a120b85 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -1,9 +1,19 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=tgunn TARGET=24Q3
flag {
name: "telecom_resolve_hidden_dependencies"
+ is_exported: true
namespace: "telecom"
description: "Mainland cleanup for hidden dependencies"
bug: "323414215"
}
+
+flag {
+ name: "telecom_mainline_blocked_numbers_manager"
+ namespace: "telecom"
+ description: "Fixed read only flag used for setting up BlockedNumbersManager to be retrieved via context"
+ bug: "325049252"
+ is_fixed_read_only: true
+}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 13577bb..f126bf3 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=yeabkal TARGET=24Q2
flag {
name: "use_device_provided_serialized_ringer_vibration"
namespace: "telecom"
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
index 854568b..1891423 100644
--- a/flags/telecom_work_profile_flags.aconfig
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -1,6 +1,7 @@
package: "com.android.server.telecom.flags"
container: "system"
+# OWNER=pmadapurmath TARGET=24Q3
flag {
name: "associated_user_refactor_for_work_profile"
namespace: "telecom"
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 44645dc..df2c70c 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -27,7 +27,7 @@
<string name="notification_missedCall_call_back" msgid="7900333283939789732">"फेरि कल गर्नुहोस्"</string>
<string name="notification_missedCall_message" msgid="4054698824390076431">"सन्देश"</string>
<string name="notification_disconnectedCall_title" msgid="1790131923692416928">"विच्छेद गरिएको कल"</string>
- <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल विच्छेद गरियो।"</string>
+ <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल डिस्कनेक्ट गरियो।"</string>
<string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"आपत्कालीन कल जारी रहेको हुनाले तपाईंको कल विच्छेद गरिएको छ।"</string>
<string name="notification_audioProcessing_title" msgid="1619035039880584575">"पृष्ठभूमिको कल"</string>
<string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> ले ब्याकग्राउन्डमा कुनै कल प्रोसेस गर्दै छ। यो एपले तपाईंको कलको अडियो प्रयोग गरिरहेको र सोही अडियो प्ले गरिरहेको हुन सक्छ।"</string>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 5b81fac..b94b9e4 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -23,6 +23,11 @@
<item name="android:actionOverflowButtonStyle">@style/TelecomDialerSettingsActionOverflowButtonStyle</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowContentOverlay">@null</item>
+
+ <!--
+ TODO(b/309578419): Make activities handle insets properly and then remove this.
+ -->
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
<style name="Theme.Telecom.BlockedNumbers" parent="@android:style/Theme.DeviceDefault.Light">
@@ -31,6 +36,11 @@
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:listDivider">@null</item>
+
+ <!--
+ TODO(b/309578419): Make activities handle insets properly and then remove this.
+ -->
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>
\ No newline at end of file
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ae2e79..70f9bfc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -133,5 +133,5 @@
<string name="callendpoint_name_unknown" msgid="2199074708477193852">"Không xác định"</string>
<string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
<string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
- <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển đổi tại đây"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển qua thiết bị này"</string>
</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index bf30720..ae5d88e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -80,6 +80,16 @@
callers are combined into a single toggle. -->
<bool name="combine_options_to_block_unavailable_and_unknown_callers">true</bool>
- <!-- System bluetooth stack package name -->
- <string name="system_bluetooth_stack">com.android.bluetooth</string>
+ <!-- When true, skip fetching quick reply response -->
+ <bool name="skip_loading_canned_text_response">false</bool>
+
+ <!-- When true, skip fetching incoming caller info -->
+ <bool name="skip_incoming_caller_info_query">false</bool>
+
+ <string-array name="system_bluetooth_stack_package_name" translatable="false">
+ <!-- AOSP -->
+ <item>com.android.bluetooth</item>
+ <!-- Used for internal targets -->
+ <item>com.google.android.bluetooth</item>
+ </string-array>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index cd608f5..0624082 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,11 @@
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
+
+ <!--
+ TODO(b/309578419): Make activities handle insets properly and then remove this.
+ -->
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
<style name="Theme.Telecom.EnableAccount" parent="Theme.Telecom.DialerSettings">
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index 45e3340..4cf54ed 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -720,15 +720,19 @@
}
private static int getCarrierId(Context context) {
- SubscriptionManager subscriptionManager =
- context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
- List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
- if (subInfos == null) {
+ try {
+ SubscriptionManager subscriptionManager =
+ context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
+ List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
+ if (subInfos == null) {
+ return -1;
+ }
+ return subInfos.stream()
+ .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
+ .map(SubscriptionInfo::getCarrierId).orElse(-1);
+ } catch (UnsupportedOperationException ignored) {
return -1;
}
- return subInfos.stream()
- .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
- .map(SubscriptionInfo::getCarrierId).orElse(-1);
}
// Copied over from Telephony's server-side logic for consistency
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index cdf44a8..8a5e858 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -23,11 +23,15 @@
import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
import android.annotation.IntDef;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.telecom.Log;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -38,6 +42,7 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -46,9 +51,10 @@
public static class Factory {
private final ScheduledExecutorService mScheduledExecutorService =
new ScheduledThreadPoolExecutor(1);
- private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
+ private CompletableFuture<AudioRoute> mAudioRouteFuture;
public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
AudioManager audioManager) throws RuntimeException {
+ mAudioRouteFuture = new CompletableFuture();
createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
try {
return mAudioRouteFuture.get();
@@ -58,8 +64,10 @@
}
private void createRetry(@AudioRouteType int type, String bluetoothAddress,
AudioManager audioManager, int retryCount) {
+ // Early exit if exceeded max number of retries (and complete the future).
if (retryCount == 0) {
mAudioRouteFuture.complete(null);
+ return;
}
Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
@@ -81,10 +89,17 @@
}
}
}
- if (routeInfo == null) {
- mScheduledExecutorService.schedule(
- () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
- RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+ // Try connecting BT device anyway (to handle wearables not showing as available
+ // communication device or LE device not showing up since it may not be the lead
+ // device).
+ if (routeInfo == null && bluetoothAddress == null) {
+ try {
+ mScheduledExecutorService.schedule(
+ () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+ RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+ } catch (RejectedExecutionException e) {
+ Log.e(this, e, "Could not schedule retry for audio routing.");
+ }
} else {
mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
}
@@ -212,7 +227,7 @@
AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
}
- int getType() {
+ public int getType() {
return mAudioRouteType;
}
@@ -222,29 +237,79 @@
// Invoked when entered pending route whose dest route is this route
void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
- AudioManager audioManager) {
+ BluetoothDevice device, AudioManager audioManager,
+ BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
+ Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (pendingAudioRoute.isActive() && !active) {
- Log.i(this, "clearCommunicationDevice");
- audioManager.clearCommunicationDevice();
+ clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
} else if (active) {
- if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
- pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+ // Handle BT routing case.
+ if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+ boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
+ audioManager, bluetoothRouteManager);
+ // Special handling for SCO case.
+ if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+ // Check if the communication device was set for the device, even if
+ // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
+ // successfully established.
+ if (connectedBtAudio || isScoAudioConnected) {
+ pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+ if (!isScoAudioConnected) {
+ pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+ }
+ } else {
+ pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ mBluetoothAddress), mBluetoothAddress);
+ }
+ return;
+ }
} else if (mAudioRouteType == TYPE_SPEAKER) {
- pendingAudioRoute.addMessage(SPEAKER_ON);
+ pendingAudioRoute.addMessage(SPEAKER_ON, null);
}
- if (!audioManager.setCommunicationDevice(mInfo)) {
- pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);
+
+ boolean result = false;
+ List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
+ for (AudioDeviceInfo deviceInfo : devices) {
+ // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
+ // mInfo accordingly.
+ if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
+ .equals(deviceInfo.getAddress())) {
+ mInfo = deviceInfo;
+ }
+ if (deviceInfo.equals(mInfo)) {
+ result = audioManager.setCommunicationDevice(mInfo);
+ if (result) {
+ pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+ }
+ Log.i(this, "Result of setting communication device for audio "
+ + "route (%s) - %b", this, result);
+ break;
+ }
+ }
+
+ // It's possible that BluetoothStateReceiver needs to report that the device is active
+ // before being able to successfully set the communication device. Refrain from sending
+ // pending route failed message for BT route until the second attempt fails.
+ if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+ pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
}
}
}
+ // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+ // sending SPEAKER_OFF, or disconnecting SCO).
void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
- AudioManager audioManager) {
+ AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+ Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
if (active) {
- if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
- pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
- } else if (mAudioRouteType == TYPE_SPEAKER) {
- pendingAudioRoute.addMessage(SPEAKER_OFF);
+ if (mAudioRouteType == TYPE_SPEAKER) {
+ pendingAudioRoute.addMessage(SPEAKER_OFF, null);
+ }
+ int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
+ audioManager);
+ // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
+ if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
+ pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
}
}
}
@@ -282,4 +347,50 @@
+ ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
+ "]";
}
+
+ private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
+ AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+ // Ensure that if another BT device was set, it is disconnected before connecting
+ // the new one.
+ AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
+ if (currentRoute.getBluetoothAddress() != null &&
+ !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
+ clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+ }
+
+ // Connect to the device (explicit handling for HFP devices).
+ boolean success = false;
+ if (device != null) {
+ success = bluetoothRouteManager.getDeviceManager()
+ .connectAudio(device, mAudioRouteType);
+ }
+
+ Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
+ this, success);
+ return success;
+ }
+
+ int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+ BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
+ // Try to see if there's a previously set device for communication that should be cleared.
+ // This only serves to help in the SCO case to ensure that we disconnect the headset.
+ if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) {
+ return -1;
+ }
+
+ int result = BluetoothStatusCodes.SUCCESS;
+ if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
+ Log.i(this, "Disconnecting SCO device.");
+ result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+ } else {
+ Log.i(this, "Clearing communication device for audio type %d.",
+ pendingAudioRoute.getCommunicationDeviceType());
+ audioManager.clearCommunicationDevice();
+ }
+
+ if (result == BluetoothStatusCodes.SUCCESS) {
+ pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+ }
+ return result;
+ }
}
diff --git a/src/com/android/server/telecom/CachedAvailableEndpointsChange.java b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
new file mode 100644
index 0000000..232f00d
--- /dev/null
+++ b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.CallEndpoint;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class CachedAvailableEndpointsChange implements CachedCallback {
+ public static final String ID = CachedAvailableEndpointsChange.class.getSimpleName();
+ Set<CallEndpoint> mAvailableEndpoints;
+
+ public Set<CallEndpoint> getAvailableEndpoints() {
+ return mAvailableEndpoints;
+ }
+
+ public CachedAvailableEndpointsChange(Set<CallEndpoint> endpoints) {
+ mAvailableEndpoints = endpoints;
+ }
+
+ @Override
+ public void executeCallback(CallSourceService service, Call call) {
+ service.onAvailableCallEndpointsChanged(call, mAvailableEndpoints);
+ }
+
+ @Override
+ public String getCallbackId() {
+ return ID;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mAvailableEndpoints);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof CachedAvailableEndpointsChange other)) {
+ return false;
+ }
+ if (mAvailableEndpoints.size() != other.mAvailableEndpoints.size()) {
+ return false;
+ }
+ for (CallEndpoint e : mAvailableEndpoints) {
+ if (!other.getAvailableEndpoints().contains(e)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
diff --git a/src/com/android/server/telecom/CachedCallback.java b/src/com/android/server/telecom/CachedCallback.java
new file mode 100644
index 0000000..88dad07
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCallback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+/**
+ * Any android.telecom.Call service (e.g. ConnectionService, TransactionalService) that declares
+ * a {@link CallSourceService} should implement this interface in order to cache the callback.
+ * The callback will be executed once the service is set.
+ */
+public interface CachedCallback {
+ /**
+ * This method executes the callback that was cached because the service was not available
+ * at the time the callback was ready.
+ *
+ * @param service that was recently set (e.g. ConnectionService)
+ * @param call that had a null service at the time the callback was ready. The service is now
+ * non-null in the call and can be executed/
+ */
+ void executeCallback(CallSourceService service, Call call);
+
+ /**
+ * This method is helpful for caching the callbacks. If the callback is called multiple times
+ * while the service is not set, ONLY the last callback should be sent to the client since the
+ * last callback is the most relevant
+ *
+ * @return the callback id that is used in a map to only store the last callback value
+ */
+ String getCallbackId();
+}
diff --git a/src/com/android/server/telecom/CachedCurrentEndpointChange.java b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
new file mode 100644
index 0000000..0d5bac9
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.CallEndpoint;
+
+import java.util.Objects;
+
+public class CachedCurrentEndpointChange implements CachedCallback {
+ public static final String ID = CachedCurrentEndpointChange.class.getSimpleName();
+ CallEndpoint mCurrentCallEndpoint;
+
+ public CallEndpoint getCurrentCallEndpoint() {
+ return mCurrentCallEndpoint;
+ }
+
+ public CachedCurrentEndpointChange(CallEndpoint callEndpoint) {
+ mCurrentCallEndpoint = callEndpoint;
+ }
+
+ @Override
+ public void executeCallback(CallSourceService service, Call call) {
+ service.onCallEndpointChanged(call, mCurrentCallEndpoint);
+ }
+
+ @Override
+ public String getCallbackId() {
+ return ID;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mCurrentCallEndpoint);
+ }
+
+ @Override
+ public boolean equals(Object obj){
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof CachedCurrentEndpointChange other)) {
+ return false;
+ }
+ return mCurrentCallEndpoint.equals(other.mCurrentCallEndpoint);
+ }
+}
+
diff --git a/src/com/android/server/telecom/CachedMuteStateChange.java b/src/com/android/server/telecom/CachedMuteStateChange.java
new file mode 100644
index 0000000..45cbfaa
--- /dev/null
+++ b/src/com/android/server/telecom/CachedMuteStateChange.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+public class CachedMuteStateChange implements CachedCallback {
+ public static final String ID = CachedMuteStateChange.class.getSimpleName();
+ boolean mIsMuted;
+
+ public boolean isMuted() {
+ return mIsMuted;
+ }
+
+ public CachedMuteStateChange(boolean isMuted) {
+ mIsMuted = isMuted;
+ }
+
+ @Override
+ public void executeCallback(CallSourceService service, Call call) {
+ service.onMuteStateChanged(call, mIsMuted);
+ }
+
+ @Override
+ public String getCallbackId() {
+ return ID;
+ }
+
+ @Override
+ public int hashCode() {
+ return Boolean.hashCode(mIsMuted);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof CachedMuteStateChange other)) {
+ return false;
+ }
+ return mIsMuted == other.mIsMuted;
+ }
+}
+
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
new file mode 100644
index 0000000..0892c33
--- /dev/null
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
+
+import android.telecom.Log;
+
+public class CachedVideoStateChange implements CachedCallback {
+ public static final String ID = CachedVideoStateChange.class.getSimpleName();
+ int mCurrentVideoState;
+
+ public int getCurrentCallEndpoint() {
+ return mCurrentVideoState;
+ }
+
+ public CachedVideoStateChange(int videoState) {
+ mCurrentVideoState = videoState;
+ }
+
+ @Override
+ public void executeCallback(CallSourceService service, Call call) {
+ service.onVideoStateChanged(call, mCurrentVideoState);
+ Log.addEvent(call, LogUtils.Events.VIDEO_STATE_CHANGED,
+ TransactionalVideoStateToString(mCurrentVideoState));
+ }
+
+ @Override
+ public String getCallbackId() {
+ return ID;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCurrentVideoState;
+ }
+
+ @Override
+ public boolean equals(Object obj){
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof CachedVideoStateChange other)) {
+ return false;
+ }
+ return mCurrentVideoState == other.mCurrentVideoState;
+ }
+}
+
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index f7ad93f..760028d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,7 @@
import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
import android.annotation.NonNull;
@@ -86,6 +87,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -824,7 +826,22 @@
* disconnect message via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} or
* {@link CallDiagnostics#onCallDisconnected(int, int)}.
*/
- private CompletableFuture<Boolean> mDisconnectFuture;
+ private CompletableFuture<Boolean> mDiagnosticCompleteFuture;
+
+ /**
+ * {@link CompletableFuture} used to perform disconnect operations after
+ * {@link #mDiagnosticCompleteFuture} has completed.
+ */
+ private CompletableFuture<Void> mDisconnectFuture;
+
+ /**
+ * {@link CompletableFuture} used to perform call removal operations after the
+ * {@link #mDisconnectFuture} has completed.
+ * <p>
+ * Note: It is possible for this future to be cancelled in the case that an internal operation
+ * will be handling clean up. (See {@link #setState}.)
+ */
+ private CompletableFuture<Void> mRemovalFuture;
/**
* {@link CompletableFuture} used to delay audio routing change for a ringing call until the
@@ -833,6 +850,16 @@
*/
private CompletableFuture<Boolean> mBtIcsFuture;
+ Map<String, CachedCallback> mCachedServiceCallbacks = new HashMap<>();
+
+ public void cacheServiceCallback(CachedCallback callback) {
+ mCachedServiceCallbacks.put(callback.getCallbackId(), callback);
+ }
+
+ public Map<String, CachedCallback> getCachedServiceCallbacks() {
+ return mCachedServiceCallbacks;
+ }
+
private FeatureFlags mFlags;
/**
@@ -1304,7 +1331,7 @@
message, null));
}
- mDisconnectFuture.complete(true);
+ mDiagnosticCompleteFuture.complete(true);
} else {
Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
getId());
@@ -1326,6 +1353,12 @@
if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
Log.w(this, "continuing processing disconnected call with another service");
+ if (mFlags.cancelRemovalOnEmergencyRedial() && isDisconnectHandledViaFuture()
+ && isRemovalPending()) {
+ Log.i(this, "cancelling removal future in favor of "
+ + "CreateConnectionProcessor handling removal");
+ mRemovalFuture.cancel(true);
+ }
mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
return false;
} else if (newState == CallState.ANSWERED && mState == CallState.ACTIVE) {
@@ -1562,6 +1595,9 @@
mIsEmergencyCall = mHandle != null &&
getTelephonyManager().isEmergencyNumber(
mHandle.getSchemeSpecificPart());
+ } catch (UnsupportedOperationException use) {
+ Log.i(this, "setHandle: no FEATURE_TELEPHONY; emergency state unknown.");
+ mIsEmergencyCall = false;
} catch (IllegalStateException ise) {
Log.e(this, ise, "setHandle: can't determine if number is emergency");
mIsEmergencyCall = false;
@@ -1575,7 +1611,11 @@
mIsTestEmergencyCall = mHandle != null &&
isTestEmergencyCall(mHandle.getSchemeSpecificPart());
}
- startCallerInfoLookup();
+ if (!mContext.getResources().getBoolean(R.bool.skip_incoming_caller_info_query)) {
+ startCallerInfoLookup();
+ } else {
+ Log.i(this, "skip incoming caller info lookup");
+ }
for (Listener l : mListeners) {
l.onHandleChanged(this);
}
@@ -1590,6 +1630,9 @@
.anyMatch(eNumber ->
eNumber.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST) &&
number.equals(eNumber.getNumber()));
+ } catch (UnsupportedOperationException uoe) {
+ // No Telephony feature, so unable to determine.
+ return false;
} catch (IllegalStateException ise) {
return false;
} catch (RuntimeException r) {
@@ -2001,7 +2044,27 @@
}
public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
+ Log.i(this, "setTransactionServiceWrapper: service=[%s]", service);
mTransactionalService = service;
+ processCachedCallbacks(service);
+ }
+
+ private void processCachedCallbacks(CallSourceService service) {
+ if(mFlags.cacheCallAudioCallbacks()) {
+ for (CachedCallback callback : mCachedServiceCallbacks.values()) {
+ callback.executeCallback(service, this);
+ }
+ // clear list for memory cleanup purposes. The Service should never be reset
+ mCachedServiceCallbacks.clear();
+ }
+ }
+
+ public CallSourceService getService() {
+ if (isTransactionalCall()) {
+ return mTransactionalService;
+ } else {
+ return mConnectionService;
+ }
}
public TransactionalServiceWrapper getTransactionServiceWrapper() {
@@ -2083,7 +2146,7 @@
userHandle = mTargetPhoneAccountHandle.getUserHandle();
}
if (userHandle != null) {
- isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
+ isWorkCall = UserUtil.isManagedProfile(mContext, userHandle, mFlags);
}
isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
@@ -2408,6 +2471,7 @@
@VisibleForTesting
public void setConnectionService(ConnectionServiceWrapper service) {
+ Log.i(this, "setConnectionService: service=[%s]", service);
setConnectionService(service, null);
}
@@ -2430,6 +2494,7 @@
mConnectionService = service;
mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
mConnectionService.addCall(this);
+ processCachedCallbacks(service);
}
/**
@@ -2511,7 +2576,7 @@
return;
}
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
- phoneAccountRegistrar, mContext, mFlags);
+ phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
mCreateConnectionProcessor.process();
}
@@ -3053,16 +3118,24 @@
public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
boolean shouldDisconnectUponTimeout, String callingMethod) {
TransactionManager tm = TransactionManager.getInstance();
- tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager,
- this, targetCallState, shouldDisconnectUponTimeout), new OutcomeReceiver<>() {
+ tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
+ this, targetCallState), new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
+ Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
+ + " due to CallException=[%s]", callingMethod, result);
}
@Override
public void onError(CallException e) {
Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onError"
+ " due to CallException=[%s]", callingMethod, e);
+ if (shouldDisconnectUponTimeout) {
+ mCallsManager.markCallAsDisconnected(Call.this,
+ new DisconnectCause(DisconnectCause.ERROR,
+ "did not hold in timeout window"));
+ mCallsManager.markCallAsRemoved(Call.this);
+ }
}
});
}
@@ -3432,7 +3505,7 @@
Log.w(this, "pullExternalCall = pullExternalCall - call %s is external but can not be"
+ " pulled while an emergency call is in progress.", mId);
mToastFactory.makeText(mContext, R.string.toast_emergency_can_not_pull_call,
- Toast.LENGTH_LONG).show();
+ Toast.LENGTH_LONG);
return;
}
@@ -3679,6 +3752,11 @@
* SMSes to that number will silently fail.
*/
public boolean isRespondViaSmsCapable() {
+ if (mContext.getResources().getBoolean(R.bool.skip_loading_canned_text_response)) {
+ Log.d(this, "maybeLoadCannedSmsResponses: skip loading due to setting");
+ return false;
+ }
+
if (mState != CallState.RINGING) {
return false;
}
@@ -3699,8 +3777,12 @@
}
// Is there a valid SMS application on the phone?
- if (mContext.getSystemService(TelephonyManager.class)
- .getAndUpdateDefaultRespondViaMessageApplication() == null) {
+ try {
+ if (mContext.getSystemService(TelephonyManager.class)
+ .getAndUpdateDefaultRespondViaMessageApplication() == null) {
+ return false;
+ }
+ } catch (UnsupportedOperationException uoe) {
return false;
}
@@ -4054,14 +4136,8 @@
videoState = VideoProfile.STATE_AUDIO_ONLY;
}
- // Transactional calls have the ability to change video calling capabilities on a per-call
- // basis as opposed to ConnectionService calls which are only based on the PhoneAccount.
- if (mFlags.transactionalVideoState()
- && mIsTransactionalCall && !mTransactionalCallSupportsVideoCalling) {
- Log.i(this, "setVideoState: The transactional does NOT support video calling."
- + " defaulted to audio (video not supported)");
- videoState = VideoProfile.STATE_AUDIO_ONLY;
- }
+ // TODO:: b/338280297. If a transactional call does not have the
+ // CallAttributes.SUPPORTS_VIDEO_CALLING capability, the videoState should be set to audio
// Track Video State history during the duration of the call.
// Only update the history when the call is active or disconnected. This ensures we do
@@ -4078,17 +4154,24 @@
int previousVideoState = mVideoState;
mVideoState = videoState;
if (mVideoState != previousVideoState) {
- Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
- VideoProfile.videoStateToString(videoState));
+ if (!mIsTransactionalCall) {
+ Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+ VideoProfile.videoStateToString(videoState));
+ }
for (Listener l : mListeners) {
l.onVideoStateChanged(this, previousVideoState, mVideoState);
}
}
- if (mFlags.transactionalVideoState()
- && mIsTransactionalCall && mTransactionalService != null) {
+ if (mFlags.transactionalVideoState() && mIsTransactionalCall) {
int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
- mTransactionalService.onVideoStateChanged(this, transactionalVS);
+ if (mTransactionalService != null) {
+ Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+ TransactionalVideoStateToString(transactionalVS));
+ mTransactionalService.onVideoStateChanged(this, transactionalVS);
+ } else {
+ cacheServiceCallback(new CachedVideoStateChange(transactionalVS));
+ }
}
if (VideoProfile.isVideo(videoState)) {
@@ -4696,17 +4779,17 @@
* @param timeoutMillis Timeout we use for waiting for the response.
* @return the {@link CompletableFuture}.
*/
- public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
- if (mDisconnectFuture == null) {
- mDisconnectFuture = new CompletableFuture<Boolean>()
+ public CompletableFuture<Boolean> initializeDiagnosticCompleteFuture(long timeoutMillis) {
+ if (mDiagnosticCompleteFuture == null) {
+ mDiagnosticCompleteFuture = new CompletableFuture<Boolean>()
.completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
// After all the chained stuff we will report where the CDS timed out.
- mDisconnectFuture.thenRunAsync(() -> {
+ mDiagnosticCompleteFuture.thenRunAsync(() -> {
if (!mReceivedCallDiagnosticPostCallResponse) {
Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
}
// Clear the future as a final step.
- mDisconnectFuture = null;
+ mDiagnosticCompleteFuture = null;
},
new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
.exceptionally((throwable) -> {
@@ -4714,14 +4797,14 @@
return null;
});
}
- return mDisconnectFuture;
+ return mDiagnosticCompleteFuture;
}
/**
* @return the disconnect future, if initialized. Used for chaining operations after creation.
*/
- public CompletableFuture<Boolean> getDisconnectFuture() {
- return mDisconnectFuture;
+ public CompletableFuture<Boolean> getDiagnosticCompleteFuture() {
+ return mDiagnosticCompleteFuture;
}
/**
@@ -4729,7 +4812,7 @@
* if this is handled immediately.
*/
public boolean isDisconnectHandledViaFuture() {
- return mDisconnectFuture != null;
+ return mDiagnosticCompleteFuture != null;
}
/**
@@ -4737,13 +4820,42 @@
* {@code cleanupStuckCalls} request.
*/
public void cleanup() {
- if (mDisconnectFuture != null) {
- mDisconnectFuture.complete(false);
- mDisconnectFuture = null;
+ if (mDiagnosticCompleteFuture != null) {
+ mDiagnosticCompleteFuture.complete(false);
+ mDiagnosticCompleteFuture = null;
}
}
/**
+ * Set the pending future to use when the call is disconnected.
+ */
+ public void setDisconnectFuture(CompletableFuture<Void> future) {
+ mDisconnectFuture = future;
+ }
+
+ /**
+ * @return The future that will be executed when the call is disconnected.
+ */
+ public CompletableFuture<Void> getDisconnectFuture() {
+ return mDisconnectFuture;
+ }
+
+ /**
+ * Set the future that will be used when call removal is taking place.
+ */
+ public void setRemovalFuture(CompletableFuture<Void> future) {
+ mRemovalFuture = future;
+ }
+
+ /**
+ * @return {@code true} if there is a pending removal operation that hasn't taken place yet, or
+ * {@code false} if there is no removal pending.
+ */
+ public boolean isRemovalPending() {
+ return mRemovalFuture != null && !mRemovalFuture.isDone();
+ }
+
+ /**
* Set the bluetooth {@link android.telecom.InCallService} binding completion or timeout future
* which is used to delay the audio routing change after the bluetooth stack get notified about
* the ringing calls.
@@ -4754,12 +4866,20 @@
}
/**
+ * @return The binding {@link CompletableFuture} for the BT ICS.
+ */
+ public CompletableFuture<Boolean> getBtIcsFuture() {
+ return mBtIcsFuture;
+ }
+
+ /**
* Wait for bluetooth {@link android.telecom.InCallService} binding completion or timeout. Used
* for audio routing operations for a ringing call.
*/
public void waitForBtIcs() {
if (mBtIcsFuture != null) {
try {
+ Log.i(this, "waitForBtIcs: waiting for BT service to bind");
mBtIcsFuture.get();
} catch (InterruptedException | ExecutionException e) {
// ignore
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 3a05eb5..8130685 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -31,6 +31,8 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
/**
* Helper class used to keep track of the requested communication device within Telecom for audio
@@ -47,7 +49,7 @@
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);
+ private final Lock mLock = new ReentrantLock();
public CallAudioCommunicationDeviceTracker(Context context) {
mAudioManager = context.getSystemService(AudioManager.class);
@@ -58,11 +60,29 @@
}
public boolean isAudioDeviceSetForType(int audioDeviceType) {
- return mAudioDeviceType == audioDeviceType;
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return mAudioDeviceType == audioDeviceType;
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
}
public int getCurrentLocallyRequestedCommunicationDevice() {
- return mAudioDeviceType;
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return mAudioDeviceType;
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
}
@VisibleForTesting
@@ -71,13 +91,22 @@
}
public void clearBtCommunicationDevice() {
- if (mBtAudioDevice == null) {
- Log.i(this, "No bluetooth device was set for communication that can be cleared.");
- return;
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
}
- // If mBtAudioDevice is set, we know a BT audio device was set for communication so
- // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
- clearCommunicationDevice(mAudioDeviceType);
+ try {
+ if (mBtAudioDevice == null) {
+ Log.i(this, "No bluetooth device was set for communication that can be cleared.");
+ } else {
+ // If mBtAudioDevice is set, we know a BT audio device was set for communication so
+ // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
+ processClearCommunicationDevice(mAudioDeviceType);
+ }
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
}
/*
@@ -93,8 +122,19 @@
public boolean setCommunicationDevice(int audioDeviceType,
BluetoothDevice btDevice) {
if (Flags.communicationDeviceProtectedByLock()) {
- mLock.tryAcquire();
+ mLock.lock();
}
+ try {
+ return processSetCommunicationDevice(audioDeviceType, btDevice);
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ private boolean processSetCommunicationDevice(int audioDeviceType,
+ BluetoothDevice btDevice) {
// There is only one audio device type associated with each type of BT device.
boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
@@ -132,14 +172,14 @@
Log.i(this, "No active device of type(s) %s available",
audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_USB_HEADSET)
+ AudioDeviceInfo.TYPE_USB_HEADSET)
: audioDeviceType);
return false;
}
// Force clear previous communication device, if one was set, before setting the new device.
if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
- clearCommunicationDevice(mAudioDeviceType);
+ processClearCommunicationDevice(mAudioDeviceType);
}
// Turn activeDevice ON.
@@ -161,12 +201,8 @@
mBtAudioDevice = null;
}
}
- if (Flags.communicationDeviceProtectedByLock()) {
- mLock.release();
- }
return result;
}
-
/*
* Clears the communication device for the passed in audio device types, given that the device
* has previously been set for communication.
@@ -174,8 +210,23 @@
*/
public void clearCommunicationDevice(int audioDeviceType) {
if (Flags.communicationDeviceProtectedByLock()) {
- mLock.tryAcquire();
+ mLock.lock();
}
+ try {
+ processClearCommunicationDevice(audioDeviceType);
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ public void processClearCommunicationDevice(int audioDeviceType) {
+ if (audioDeviceType == sAUDIO_DEVICE_TYPE_INVALID) {
+ Log.i(this, "clearCommunicationDevice: Skip clearing communication device"
+ + "for invalid audio type (-1).");
+ }
+
// There is only one audio device type associated with each type of BT device.
boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
@@ -184,10 +235,10 @@
if (audioDeviceType != mAudioDeviceType
&& !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
Log.i(this, "Unable to clear communication device of type(s), %s. "
- + "Device does not correspond to the locally requested device type.",
+ + "Device does not correspond to the locally requested device type.",
audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_USB_HEADSET)
+ AudioDeviceInfo.TYPE_USB_HEADSET)
: audioDeviceType
);
return;
@@ -207,13 +258,10 @@
mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
mBtAudioDevice = null;
}
- if (Flags.communicationDeviceProtectedByLock()) {
- mLock.release();
- }
}
private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) {
- return audioDeviceType != AudioDeviceInfo.TYPE_WIRED_HEADSET
- ? false : sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
+ return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+ && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
}
}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index e5678a0..1f1ca9d 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -20,6 +20,8 @@
import android.content.Context;
import android.media.IAudioService;
import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
@@ -36,6 +38,7 @@
import java.util.HashSet;
import java.util.Set;
import java.util.LinkedHashSet;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@@ -66,9 +69,13 @@
private Call mStreamingCall;
private Call mForegroundCall;
+ private CompletableFuture<Boolean> mCallRingingFuture;
+ private Thread mBtIcsBindingThread;
private boolean mIsTonePlaying = false;
private boolean mIsDisconnectedTonePlaying = false;
private InCallTonePlayer mHoldTonePlayer;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter,
CallsManager callsManager,
@@ -105,6 +112,9 @@
mBluetoothStateReceiver = bluetoothStateReceiver;
mDtmfLocalTonePlayer = dtmfLocalTonePlayer;
mFeatureFlags = featureFlags;
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mPlayerFactory.setCallAudioManager(this);
mCallAudioModeStateMachine.setCallAudioManager(this);
@@ -750,14 +760,42 @@
private void onCallEnteringRinging() {
if (mRingingCalls.size() == 1) {
- // Wait until the BT ICS binding completed to request further audio route change
- for (Call ringingCall: mRingingCalls) {
- ringingCall.waitForBtIcs();
+ Log.i(this, "onCallEnteringRinging: mFeatureFlags.separatelyBindToBtIncallService() ? %s",
+ mFeatureFlags.separatelyBindToBtIncallService());
+ Log.i(this, "onCallEnteringRinging: mRingingCalls.getFirst().getBtIcsFuture() = %s",
+ mRingingCalls.getFirst().getBtIcsFuture());
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && mRingingCalls.getFirst().getBtIcsFuture() != null) {
+ mCallRingingFuture = mRingingCalls.getFirst().getBtIcsFuture()
+ .thenComposeAsync((completed) -> {
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.NEW_RINGING_CALL,
+ makeArgsForModeStateMachine());
+ return CompletableFuture.completedFuture(completed);
+ }, new LoggedHandlerExecutor(mHandler, "CAM.oCER", mCallsManager.getLock()))
+ .exceptionally((throwable) -> {
+ Log.e(this, throwable, "Error while executing BT ICS future");
+ // Fallback on performing computation on a separate thread.
+ handleBtBindingWaitFallback();
+ return null;
+ });
+ } else {
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.NEW_RINGING_CALL,
+ makeArgsForModeStateMachine());
}
+ }
+ }
+
+ private void handleBtBindingWaitFallback() {
+ // Wait until the BT ICS binding completed to request further audio route change
+ mBtIcsBindingThread = new Thread(() -> {
+ mRingingCalls.getFirst().waitForBtIcs();
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_RINGING_CALL,
makeArgsForModeStateMachine());
- }
+ });
+ mBtIcsBindingThread.start();
}
private void onCallEnteringHold() {
@@ -889,12 +927,14 @@
// we will not play a disconnect tone.
if (call.isHandoverInProgress()) {
Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call);
+ completeDisconnectToneFuture(call);
return;
}
if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
" and there is another call.");
+ completeDisconnectToneFuture(call);
return;
}
@@ -935,6 +975,8 @@
mCallsManager.onDisconnectedTonePlaying(call, true);
mIsDisconnectedTonePlaying = true;
}
+ } else {
+ completeDisconnectToneFuture(call);
}
}
}
@@ -1022,6 +1064,14 @@
oldState == CallState.ON_HOLD;
}
+ private void completeDisconnectToneFuture(Call call) {
+ CompletableFuture<Void> disconnectedToneFuture = mCallsManager.getInCallController()
+ .getDisconnectedToneBtFutures().get(call.getId());
+ if (disconnectedToneFuture != null) {
+ disconnectedToneFuture.complete(null);
+ }
+ }
+
@VisibleForTesting
public Set<Call> getTrackedCalls() {
return mCalls;
@@ -1031,4 +1081,9 @@
public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
return mCallStateToCalls;
}
+
+ @VisibleForTesting
+ public CompletableFuture<Boolean> getCallRingingFuture() {
+ return mCallRingingFuture;
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 5585d09..9927c22 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -134,5 +134,6 @@
CallAudioState getCurrentCallAudioState();
boolean isHfpDeviceAvailable();
Handler getAdapterHandler();
+ PendingAudioRoute getPendingAudioRoute();
void dump(IndentingPrintWriter pw);
}
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index d38577d..820219d 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -18,30 +18,32 @@
import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
-import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.UserInfo;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
-import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.telecom.VideoProfile;
import android.util.ArrayMap;
+import android.util.Pair;
import androidx.annotation.NonNull;
@@ -49,8 +51,10 @@
import com.android.internal.os.SomeArgs;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -90,8 +94,12 @@
private Map<Integer, AudioRoute> mTypeRoutes;
private PendingAudioRoute mPendingAudioRoute;
private AudioRoute.Factory mAudioRouteFactory;
+ private StatusBarNotifier mStatusBarNotifier;
+ private FeatureFlags mFeatureFlags;
private int mFocusType;
+ private boolean mIsScoAudioConnected;
private final Object mLock = new Object();
+ private final TelecomSystem.SyncRoot mTelecomLock;
private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -102,7 +110,9 @@
AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
if ((info != null) &&
(info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
- sendMessageWithSessionInfo(SPEAKER_ON);
+ if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
} else {
sendMessageWithSessionInfo(SPEAKER_OFF);
}
@@ -152,12 +162,11 @@
private boolean mIsActive;
public CallAudioRouteController(
- Context context,
- CallsManager callsManager,
+ Context context, CallsManager callsManager,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- AudioRoute.Factory audioRouteFactory,
- WiredHeadsetManager wiredHeadsetManager,
- BluetoothRouteManager bluetoothRouteManager) {
+ AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
+ BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
+ FeatureFlags featureFlags) {
mContext = context;
mCallsManager = callsManager;
mAudioManager = context.getSystemService(AudioManager.class);
@@ -166,7 +175,11 @@
mWiredHeadsetManager = wiredHeadsetManager;
mIsMute = false;
mBluetoothRouteManager = bluetoothRouteManager;
+ mStatusBarNotifier = statusBarNotifier;
+ mFeatureFlags = featureFlags;
mFocusType = NO_FOCUS;
+ mIsScoAudioConnected = false;
+ mTelecomLock = callsManager.getLock();
HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
handlerThread.start();
@@ -246,8 +259,10 @@
case USER_SWITCH_SPEAKER:
handleSwitchSpeaker();
break;
+ case SWITCH_BASELINE_ROUTE:
case USER_SWITCH_BASELINE_ROUTE:
- handleSwitchBaselineRoute();
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleSwitchBaselineRoute(address);
break;
case SPEAKER_ON:
handleSpeakerOn();
@@ -276,7 +291,7 @@
handleMuteChanged(false);
break;
case MUTE_EXTERNALLY_CHANGED:
- handleMuteChanged(mAudioManager.isMasterMute());
+ handleMuteChanged(mAudioManager.isMicrophoneMute());
break;
case SWITCH_FOCUS:
focus = msg.arg1;
@@ -296,16 +311,16 @@
@Override
public void initialize() {
mAvailableRoutes = new HashSet<>();
- mBluetoothRoutes = new ArrayMap<>();
+ mBluetoothRoutes = new LinkedHashMap<>();
mTypeRoutes = new ArrayMap<>();
mStreamingRoutes = new HashSet<>();
- mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+ mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager);
mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
mStreamingRoutes.add(mStreamingRoute);
- int supportMask = calculateSupportedRouteMask();
+ int supportMask = calculateSupportedRouteMaskInit();
if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
- // Create spekaer routes
+ // Create speaker routes
mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
mAudioManager);
if (mSpeakerDockRoute == null) {
@@ -391,7 +406,7 @@
@Override
public CallAudioState getCurrentCallAudioState() {
- return null;
+ return mCallAudioState;
}
@Override
@@ -405,6 +420,11 @@
}
@Override
+ public PendingAudioRoute getPendingAudioRoute() {
+ return mPendingAudioRoute;
+ }
+
+ @Override
public void dump(IndentingPrintWriter pw) {
}
@@ -434,6 +454,7 @@
private void routeTo(boolean active, AudioRoute destRoute) {
if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+ Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
return;
}
if (mIsPending) {
@@ -443,11 +464,13 @@
Log.i(this, "Override current pending route destination from %s(active=%b) to "
+ "%s(active=%b)",
mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+ // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
+ if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
+ mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+ }
// override pending route while keep waiting for still pending messages for the
// previous pending route
- mIsActive = active;
mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
- mPendingAudioRoute.setDestRoute(active, destRoute);
} else {
if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
return;
@@ -461,10 +484,11 @@
// Avoid waiting for pending messages for an unavailable route
mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
}
- mPendingAudioRoute.setDestRoute(active, destRoute);
- mIsActive = active;
mIsPending = true;
}
+ mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+ mIsScoAudioConnected);
+ mIsActive = active;
mPendingAudioRoute.evaluatePendingState();
postTimeoutMessage();
}
@@ -512,7 +536,7 @@
// Route to expected state
if (mCurrentRoute.equals(wiredHeadsetRoute)) {
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
@@ -551,7 +575,7 @@
// Route to expected state
if (mCurrentRoute.equals(dockRoute)) {
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
@@ -567,38 +591,71 @@
if (mCurrentRoute.equals(mStreamingRoute)) {
mCurrentRoute = DUMMY_ROUTE;
onAvailableRoutesChanged();
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
} else {
Log.i(this, "ignore disable streaming, not in streaming");
}
}
+ /**
+ * Handles the case when SCO audio is connected for the BT headset. This follows shortly after
+ * the BT device has been established as an active device (BT_ACTIVE_DEVICE_PRESENT) and doesn't
+ * apply to other BT device types. In this case, the pending audio route will process the
+ * BT_AUDIO_CONNECTED message that will trigger routing to the pending destination audio route;
+ * otherwise, routing will be ignored if there aren't pending routes to be processed.
+ *
+ * Message being handled: BT_AUDIO_CONNECTED
+ */
private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
+ Log.i(this, "handleBtAudioActive: is pending path");
if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
+ bluetoothDevice.getAddress()), null);
}
} else {
// ignore, not triggered by telecom
+ Log.i(this, "handleBtAudioActive: ignoring handling bt audio active.");
}
}
+ /**
+ * Handles the case when SCO audio is disconnected for the BT headset. In this case, the pending
+ * audio route will process the BT_AUDIO_DISCONNECTED message which will trigger routing to the
+ * pending destination audio route; otherwise, routing will be ignored if there aren't any
+ * pending routes to be processed.
+ *
+ * Message being handled: BT_AUDIO_DISCONNECTED
+ */
private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
if (mIsPending) {
+ Log.i(this, "handleBtAudioInactive: is pending path");
if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
bluetoothDevice.getAddress())) {
- mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
+ mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
+ bluetoothDevice.getAddress()), null);
}
} else {
// ignore, not triggered by telecom
+ Log.i(this, "handleBtAudioInactive: ignoring handling bt audio inactive.");
}
}
+ /**
+ * This particular routing occurs when the BT device is trying to establish itself as a
+ * connected device (refer to BluetoothStateReceiver#handleConnectionStateChanged). The device
+ * is included as an available route and cached into the current BT routes.
+ *
+ * Message being handled: BT_DEVICE_ADDED
+ */
private void handleBtConnected(@AudioRoute.AudioRouteType int type,
BluetoothDevice bluetoothDevice) {
- AudioRoute bluetoothRoute = null;
- bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+ if (containsHearingAidPair(type, bluetoothDevice)) {
+ return;
+ }
+
+ AudioRoute bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
mAudioManager);
if (bluetoothRoute == null) {
Log.w(this, "Can't find available audio device info for route type:"
@@ -611,6 +668,14 @@
}
}
+ /**
+ * Handles the case when the BT device is in a disconnecting/disconnected state. In this case,
+ * the audio route for the specified device is removed from the available BT routes and the
+ * audio is routed to an available route if the current route is pointing to the device which
+ * got disconnected.
+ *
+ * Message being handled: BT_DEVICE_REMOVED
+ */
private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
BluetoothDevice bluetoothDevice) {
// Clean up unavailable routes
@@ -624,31 +689,51 @@
// Fallback to an available route
if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
- routeTo(mIsActive, getBaseRoute(false));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
+ /**
+ * This particular routing occurs when the specified bluetooth device is marked as the active
+ * device (refer to BluetoothStateReceiver#handleActiveDeviceChanged). This takes care of
+ * moving the call audio route to the bluetooth route.
+ *
+ * Message being handled: BT_ACTIVE_DEVICE_PRESENT
+ */
private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
String deviceAddress) {
AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
if (bluetoothRoute != null) {
- Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
+ Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
mIsActive);
routeTo(mIsActive, bluetoothRoute);
+ } else {
+ Log.i(this, "request to route to unavailable bluetooth route - type (%s), address (%s)",
+ type, deviceAddress);
}
}
+ /**
+ * Handles routing for when the active BT device is removed for a given audio route type. In
+ * this case, the audio is routed to another available route if the current route hasn't been
+ * adjusted yet or there is a pending destination route associated with the device type that
+ * went inactive. Note that BT_DEVICE_REMOVED will be processed first in this case, which will
+ * handle removing the BT route for the device that went inactive as well as falling back to
+ * an available route.
+ *
+ * Message being handled: BT_ACTIVE_DEVICE_GONE
+ */
private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
|| (!mIsPending && mCurrentRoute.getType() == type)) {
// Fallback to an available route
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
}
}
private void handleMuteChanged(boolean mute) {
mIsMute = mute;
- if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+ if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
IAudioService audioService = mAudioServiceFactory.getAudioService();
Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
audioService == null);
@@ -667,30 +752,37 @@
}
private void handleSwitchFocus(int focus) {
+ Log.i(this, "handleSwitchFocus: focus (%s)", focus);
mFocusType = focus;
switch (focus) {
case NO_FOCUS -> {
if (mIsActive) {
+ // Reset mute state after call ends.
handleMuteChanged(false);
+ // Route back to inactive route.
routeTo(false, mCurrentRoute);
+ // Clear pending messages
+ mPendingAudioRoute.clearPendingMessages();
}
}
case ACTIVE_FOCUS -> {
- if (!mIsActive) {
- routeTo(true, getBaseRoute(true));
- }
+ // Route to active baseline route (we may need to change audio route in the case
+ // when a video call is put on hold).
+ routeTo(true, getBaseRoute(true, null));
}
case RINGING_FOCUS -> {
if (!mIsActive) {
- AudioRoute route = getBaseRoute(true);
+ AudioRoute route = getBaseRoute(true, null);
BluetoothDevice device = mBluetoothRoutes.get(route);
+ // Check if in-band ringtone is enabled for the device; if it isn't, move to
+ // inactive route.
if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
routeTo(false, route);
} else {
routeTo(true, route);
}
} else {
- // active
+ // Route is already active.
BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
routeTo(false, mCurrentRoute);
@@ -729,7 +821,7 @@
routeTo(mIsActive, bluetoothRoute);
}
} else {
- Log.i(this, "ignore switch bluetooth request");
+ Log.i(this, "ignore switch bluetooth request to unavailable address");
}
}
@@ -738,7 +830,7 @@
if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
routeTo(mIsActive, headsetRoute);
} else {
- Log.i(this, "ignore switch speaker request");
+ Log.i(this, "ignore switch headset request");
}
}
@@ -750,13 +842,16 @@
}
}
- private void handleSwitchBaselineRoute() {
- routeTo(mIsActive, getBaseRoute(true));
+ private void handleSwitchBaselineRoute(String btAddressToExclude) {
+ routeTo(mIsActive, getBaseRoute(true, btAddressToExclude));
}
private void handleSpeakerOn() {
if (isPending()) {
- mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+ Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
+ mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
+ // Update status bar notification if we are in a call.
+ mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
} else {
if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
routeTo(mIsActive, mSpeakerDockRoute);
@@ -771,9 +866,12 @@
private void handleSpeakerOff() {
if (isPending()) {
- mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+ Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
+ mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
+ // Update status bar notification
+ mStatusBarNotifier.notifySpeakerphone(false);
} else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
- routeTo(mIsActive, getBaseRoute(true));
+ routeTo(mIsActive, getBaseRoute(true, null));
// Since the route switching triggered by this message, we need to manually send it
// again so that we won't stuck in the pending route
if (mIsActive) {
@@ -783,12 +881,17 @@
}
}
+ /**
+ * This is invoked when there are no more pending audio routes to be processed, which signals
+ * a change for the current audio route and the call audio state to be updated accordingly.
+ */
public void handleExitPendingRoute() {
if (mIsPending) {
- Log.i(this, "Exit pending route and enter %s(active=%b)",
- mPendingAudioRoute.getDestRoute(), mIsActive);
mCurrentRoute = mPendingAudioRoute.getDestRoute();
+ Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
+ "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
mIsPending = false;
+ mPendingAudioRoute.clearPendingMessages();
onCurrentRouteChanged();
}
}
@@ -817,7 +920,21 @@
for (AudioRoute route : getAvailableRoutes()) {
routeMask |= ROUTE_MAP.get(route.getType());
if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
- availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+ BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
+ // Only include the lead device for LE audio (otherwise, the routes will show
+ // two separate devices in the UI).
+ if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
+ && getLeAudioService() != null) {
+ int groupId = getLeAudioService().getGroupId(deviceToAdd);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+ deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+ }
+ }
+ // This will only ever be null when the lead device (LE) is disconnected and
+ // try to obtain the lead device for the 2nd bud.
+ if (deviceToAdd != null) {
+ availableBluetoothDevices.add(deviceToAdd);
+ }
}
}
updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
@@ -831,10 +948,12 @@
mCallAudioState.getSupportedBluetoothDevices()));
}
- private void updateCallAudioState(CallAudioState callAudioState) {
- Log.i(this, "updateCallAudioState: " + callAudioState);
+ private void updateCallAudioState(CallAudioState newCallAudioState) {
+ Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
CallAudioState oldState = mCallAudioState;
- mCallAudioState = callAudioState;
+ mCallAudioState = newCallAudioState;
+ // Update status bar notification
+ mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
updateAudioStateForTrackedCalls(mCallAudioState);
}
@@ -866,6 +985,7 @@
// Get preferred device
AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
+ Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
if (deviceAttr == null) {
return null;
}
@@ -881,16 +1001,48 @@
}
}
- private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
- if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
- return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+ private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
+ String btAddressToExclude) {
+ boolean skipEarpiece;
+ Call foregroundCall = mCallAudioManager.getForegroundCall();
+ synchronized (mTelecomLock) {
+ skipEarpiece = foregroundCall != null
+ && VideoProfile.isVideo(foregroundCall.getVideoState());
+ }
+ // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
+ // are only wearables available.
+ AudioRoute activeWatchOrNonWatchDeviceRoute =
+ getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude);
+ if (mBluetoothRoutes.isEmpty() || !includeBluetooth
+ || activeWatchOrNonWatchDeviceRoute == null) {
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "available non-BT route.");
+ AudioRoute defaultRoute = mEarpieceWiredRoute != null
+ ? mEarpieceWiredRoute
+ : mSpeakerDockRoute;
+ // Ensure that we default to speaker route if we're in a video call, but disregard it if
+ // a wired headset is plugged in.
+ if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "speaker route for video call.");
+ defaultRoute = mSpeakerDockRoute;
+ }
+ return defaultRoute;
} else {
- // Most recent active route will always be the last in the array
- return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+ // Most recent active route will always be the last in the array (ensure that we don't
+ // auto route to a wearable device unless it's already active).
+ String autoRoutingToWatchExcerpt = mFeatureFlags.ignoreAutoRouteToWatchDevice()
+ ? " (except watch)"
+ : "";
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "most recently active BT route" + autoRoutingToWatchExcerpt + ".");
+ return activeWatchOrNonWatchDeviceRoute;
}
}
- private int calculateSupportedRouteMask() {
+ private int calculateSupportedRouteMaskInit() {
+ Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s",
+ mWiredHeadsetManager.isPluggedIn());
int routeMask = CallAudioState.ROUTE_SPEAKER;
if (mWiredHeadsetManager.isPluggedIn()) {
@@ -921,8 +1073,8 @@
return mCurrentRoute;
}
- private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
- String address) {
+ public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+ String address) {
for (AudioRoute route : mBluetoothRoutes.keySet()) {
if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
return route;
@@ -931,17 +1083,144 @@
return null;
}
- public AudioRoute getBaseRoute(boolean includeBluetooth) {
+ public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
if (destRoute == null) {
- destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+ destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
}
if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
destRoute = null;
}
+ Log.i(this, "getBaseRoute - audio routing to %s", destRoute);
return destRoute;
}
+ /**
+ * Don't add additional AudioRoute when a hearing aid pair is detected. The devices have
+ * separate addresses, so we need to perform explicit handling to ensure we don't
+ * treat them as two separate devices.
+ */
+ private boolean containsHearingAidPair(@AudioRoute.AudioRouteType int type,
+ BluetoothDevice bluetoothDevice) {
+ // Check if it is a hearing aid pair and skip connecting to the other device in this case.
+ // Traverse mBluetoothRoutes backwards as the most recently active device will be inserted
+ // last.
+ String existingHearingAidAddress = null;
+ List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+ for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+ AudioRoute audioRoute = bluetoothRoutes.get(i);
+ if (audioRoute.getType() == AudioRoute.TYPE_BLUETOOTH_HA) {
+ existingHearingAidAddress = audioRoute.getBluetoothAddress();
+ break;
+ }
+ }
+
+ // Check that route is for hearing aid and that there exists another hearing aid route
+ // created for the first device (of the pair) that was connected.
+ if (type == AudioRoute.TYPE_BLUETOOTH_HA && existingHearingAidAddress != null) {
+ BluetoothAdapter bluetoothAdapter = mBluetoothRouteManager.getDeviceManager()
+ .getBluetoothAdapter();
+ if (bluetoothAdapter != null) {
+ List<BluetoothDevice> activeHearingAids =
+ bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
+ for (BluetoothDevice hearingAid : activeHearingAids) {
+ if (hearingAid != null && hearingAid.getAddress() != null) {
+ String address = hearingAid.getAddress();
+ if (address.equals(bluetoothDevice.getAddress())
+ || address.equals(existingHearingAidAddress)) {
+ Log.i(this, "containsHearingAidPair: Detected a hearing aid "
+ + "pair, ignoring creating a new AudioRoute");
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Prevent auto routing to a wearable device when calculating the default bluetooth audio route
+ * to move to. This function ensures that the most recently active non-wearable device is
+ * selected for routing unless a wearable device has already been identified as an active
+ * device.
+ */
+ private AudioRoute getActiveWatchOrNonWatchDeviceRoute(String btAddressToExclude) {
+ if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+ Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: ignore_auto_route_to_watch_device "
+ + "flag is disabled. Routing to most recently reported active device.");
+ return getMostRecentlyActiveBtRoute(btAddressToExclude);
+ }
+
+ List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+ // Traverse the routes from the most recently active recorded devices first.
+ AudioRoute nonWatchDeviceRoute = null;
+ for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+ AudioRoute route = bluetoothRoutes.get(i);
+ BluetoothDevice device = mBluetoothRoutes.get(route);
+ // Skip excluded BT address and LE audio if it's not the lead device.
+ if (route.getBluetoothAddress().equals(btAddressToExclude)
+ || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
+ continue;
+ }
+ // Check if the most recently active device is a watch device.
+ if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
+ .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+ Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
+ bluetoothRoutes.get(0));
+ return bluetoothRoutes.get(0);
+ }
+ // Record the first occurrence of a non-watch device route if found.
+ if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
+ nonWatchDeviceRoute = route;
+ break;
+ }
+ }
+
+ Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute);
+ return nonWatchDeviceRoute;
+ }
+
+ /**
+ * Returns the most actively reported bluetooth route excluding the passed in route.
+ */
+ private AudioRoute getMostRecentlyActiveBtRoute(String btAddressToExclude) {
+ List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+ for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+ AudioRoute route = bluetoothRoutes.get(i);
+ // Skip LE route if it's not the lead device.
+ if (isLeAudioNonLeadDeviceOrServiceUnavailable(
+ route.getType(), mBluetoothRoutes.get(route))) {
+ continue;
+ }
+ if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
+ return route;
+ }
+ }
+ return null;
+ }
+
+ private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
+ BluetoothDevice device) {
+ if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
+ return false;
+ } else if (getLeAudioService() == null) {
+ return true;
+ }
+
+ int groupId = getLeAudioService().getGroupId(device);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+ BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+ Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
+ return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
+ }
+ return false;
+ }
+
+ private BluetoothLeAudio getLeAudioService() {
+ return mBluetoothRouteManager.getDeviceManager().getLeAudioService();
+ }
+
@VisibleForTesting
public void setAudioManager(AudioManager audioManager) {
mAudioManager = audioManager;
@@ -952,6 +1231,18 @@
mAudioRouteFactory = audioRouteFactory;
}
+ public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+ return mBluetoothRoutes;
+ }
+
+ public void overrideIsPending(boolean isPending) {
+ mIsPending = isPending;
+ }
+
+ public void setIsScoAudioConnected(boolean value) {
+ mIsScoAudioConnected = value;
+ }
+
@VisibleForTesting
public void setActive(boolean active) {
if (active) {
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 26c25e8..74d23a9 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -288,8 +288,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
mAvailableRoutes, null,
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -511,8 +516,13 @@
}
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -749,8 +759,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
// Do not send RINGER_MODE_CHANGE if no Bluetooth SCO audio device is available
if (mBluetoothRouteManager.getBluetoothAudioConnectedDevice() != null) {
mCallAudioManager.onRingerModeChange();
@@ -847,6 +862,14 @@
if (msg.arg1 == NO_FOCUS) {
// Only disconnect audio here instead of routing away from BT entirely.
if (mFeatureFlags.transitRouteBeforeAudioDisconnectBt()) {
+ // Note: We have to turn off mute here rather than when entering the
+ // QuiescentBluetooth route because setMuteOn will only work when there the
+ // current state is active.
+ // We don't need to do this in the unflagged path since reinitialize
+ // will turn off mute.
+ if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
+ setMuteOn(false);
+ }
transitionTo(mQuiescentBluetoothRoute);
mBluetoothRouteManager.disconnectAudio();
} else {
@@ -890,8 +913,13 @@
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -977,9 +1005,6 @@
public void enter() {
super.enter();
mHasUserExplicitlyLeftBluetooth = false;
- if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
- setMuteOn(false);
- }
updateInternalCallAudioState();
}
@@ -1117,8 +1142,13 @@
mWasOnSpeaker = true;
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -2095,7 +2125,14 @@
return base;
}
+ @Override
public Handler getAdapterHandler() {
return getHandler();
}
+
+ @Override
+ public PendingAudioRoute getPendingAudioRoute() {
+ // Only used by CallAudioRouteController.
+ return null;
+ }
}
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
index 4738cd4..49c0d51 100644
--- a/src/com/android/server/telecom/CallEndpointController.java
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -27,6 +27,7 @@
import android.telecom.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.HashMap;
import java.util.Map;
@@ -49,6 +50,7 @@
private final Context mContext;
private final CallsManager mCallsManager;
+ private final FeatureFlags mFeatureFlags;
private final HashMap<Integer, Integer> mRouteToTypeMap;
private final HashMap<Integer, Integer> mTypeToRouteMap;
private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
@@ -57,10 +59,10 @@
private ParcelUuid mRequestedEndpointId;
private CompletableFuture<Integer> mPendingChangeRequest;
- public CallEndpointController(Context context, CallsManager callsManager) {
+ public CallEndpointController(Context context, CallsManager callsManager, FeatureFlags flags) {
mContext = context;
mCallsManager = callsManager;
-
+ mFeatureFlags = flags;
mRouteToTypeMap = new HashMap<>(5);
mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
@@ -197,43 +199,91 @@
Set<Call> calls = mCallsManager.getTrackedCalls();
for (Call call : calls) {
- if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
- } else if (call != null && call.getTransactionServiceWrapper() != null) {
- call.getTransactionServiceWrapper()
- .onCallEndpointChanged(call, mActiveCallEndpoint);
+ if (mFeatureFlags.cacheCallAudioCallbacks()) {
+ onCallEndpointChangedOrCache(call);
+ } else {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper()
+ .onCallEndpointChanged(call, mActiveCallEndpoint);
+ }
}
}
}
+ private void onCallEndpointChangedOrCache(Call call) {
+ if (call == null) {
+ return;
+ }
+ CallSourceService service = call.getService();
+ if (service != null) {
+ service.onCallEndpointChanged(call, mActiveCallEndpoint);
+ } else {
+ call.cacheServiceCallback(new CachedCurrentEndpointChange(mActiveCallEndpoint));
+ }
+ }
+
private void notifyAvailableCallEndpointsChange() {
mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
Set<Call> calls = mCallsManager.getTrackedCalls();
for (Call call : calls) {
- if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onAvailableCallEndpointsChanged(call,
- mAvailableCallEndpoints);
- } else if (call != null && call.getTransactionServiceWrapper() != null) {
- call.getTransactionServiceWrapper()
- .onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+ if (mFeatureFlags.cacheCallAudioCallbacks()) {
+ onAvailableEndpointsChangedOrCache(call);
+ } else {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onAvailableCallEndpointsChanged(call,
+ mAvailableCallEndpoints);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper().onAvailableCallEndpointsChanged(call,
+ mAvailableCallEndpoints);
+ }
}
}
}
+ private void onAvailableEndpointsChangedOrCache(Call call) {
+ if (call == null) {
+ return;
+ }
+ CallSourceService service = call.getService();
+ if (service != null) {
+ service.onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+ } else {
+ call.cacheServiceCallback(new CachedAvailableEndpointsChange(mAvailableCallEndpoints));
+ }
+ }
+
private void notifyMuteStateChange(boolean isMuted) {
mCallsManager.updateMuteState(isMuted);
Set<Call> calls = mCallsManager.getTrackedCalls();
for (Call call : calls) {
- if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onMuteStateChanged(call, isMuted);
- } else if (call != null && call.getTransactionServiceWrapper() != null) {
- call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+ if (mFeatureFlags.cacheCallAudioCallbacks()) {
+ onMuteStateChangedOrCache(call, isMuted);
+ } else {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onMuteStateChanged(call, isMuted);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+ }
}
}
}
+ private void onMuteStateChangedOrCache(Call call, boolean isMuted){
+ if (call == null) {
+ return;
+ }
+ CallSourceService service = call.getService();
+ if (service != null) {
+ service.onMuteStateChanged(call, isMuted);
+ } else {
+ call.cacheServiceCallback(new CachedMuteStateChange(isMuted));
+ }
+ }
+
private void createAvailableCallEndpoints(CallAudioState state) {
Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index c02d20d..8e1f754 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -1,10 +1,16 @@
package com.android.server.telecom;
+import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
+
+import com.android.internal.app.IntentForwarderActivity;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.flags.FeatureFlags;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
@@ -20,6 +26,7 @@
import android.telecom.VideoProfile;
import android.telephony.DisconnectCause;
import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
import android.widget.Toast;
import java.util.concurrent.CompletableFuture;
@@ -168,13 +175,15 @@
if (!callsManager.isSelfManaged(phoneAccountHandle,
(UserHandle) intent.getParcelableExtra(KEY_INITIATING_USER))) {
- boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(context, intent);
+ boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(
+ context, intent, featureFlags);
// Show the toast to warn user that it is a personal call though initiated in work
// profile.
if (fixedInitiatingUser) {
if (featureFlags.telecomResolveHiddenDependencies()) {
- Toast.makeText(context, context.getString(R.string.toast_personal_call_msg),
- Toast.LENGTH_LONG).show();
+ context.getMainExecutor().execute(() ->
+ Toast.makeText(context, context.getString(
+ R.string.toast_personal_call_msg), Toast.LENGTH_LONG).show());
} else {
Toast.makeText(context, Looper.getMainLooper(),
context.getString(R.string.toast_personal_call_msg),
@@ -191,6 +200,18 @@
boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
initiatingUser.getIdentifier());
+ if (privateSpaceFlagsEnabled()) {
+ if (!callsManager.isSelfManaged(phoneAccountHandle, initiatingUser)
+ && !TelephonyUtil.shouldProcessAsEmergency(context, handle)
+ && UserUtil.isPrivateProfile(initiatingUser, context)) {
+ boolean dialogShown = maybeRedirectToIntentForwarderForPrivate(context, intent,
+ initiatingUser);
+ if (dialogShown) {
+ return;
+ }
+ }
+ }
+
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags);
@@ -226,16 +247,18 @@
*
* @return whether the initiating user is fixed.
*/
- static boolean fixInitiatingUserIfNecessary(Context context, Intent intent) {
+ static boolean fixInitiatingUserIfNecessary(Context context, Intent intent,
+ FeatureFlags featureFlags) {
final UserHandle initiatingUser = intent.getParcelableExtra(KEY_INITIATING_USER);
- if (UserUtil.isManagedProfile(context, initiatingUser)) {
+ if (UserUtil.isManagedProfile(context, initiatingUser, featureFlags)) {
boolean noDialerInstalled = DefaultDialerManager.getInstalledDialerApplications(context,
initiatingUser.getIdentifier()).size() == 0;
if (noDialerInstalled) {
- final UserManager userManager = UserManager.get(context);
- UserHandle parentUserHandle =
- userManager.getProfileParent(
- initiatingUser.getIdentifier()).getUserHandle();
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ UserHandle parentUserHandle = featureFlags.telecomResolveHiddenDependencies()
+ ? userManager.getProfileParent(initiatingUser)
+ : userManager.getProfileParent(initiatingUser.getIdentifier())
+ .getUserHandle();
intent.putExtra(KEY_INITIATING_USER, parentUserHandle);
Log.i(CallIntentProcessor.class, "fixInitiatingUserIfNecessary: no dialer installed"
@@ -306,4 +329,43 @@
context.startActivityAsUser(errorIntent, UserHandle.CURRENT);
}
}
+
+ private static boolean privateSpaceFlagsEnabled() {
+ return android.multiuser.Flags.enablePrivateSpaceFeatures()
+ && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
+ }
+
+ private static boolean maybeRedirectToIntentForwarderForPrivate(
+ Context context,
+ Intent forwardCallIntent,
+ UserHandle initiatingUser) {
+
+ // If CALL intent filters are set to SKIP_CURRENT_PROFILE, PM will resolve this to an
+ // intent forwarder activity.
+ forwardCallIntent.setComponent(null);
+ forwardCallIntent.setPackage(null);
+ ResolveInfo resolveInfos =
+ context.getPackageManager()
+ .resolveActivityAsUser(
+ forwardCallIntent,
+ PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY),
+ initiatingUser.getIdentifier());
+
+ if (resolveInfos == null
+ || !resolveInfos
+ .getComponentInfo()
+ .getComponentName()
+ .getShortClassName()
+ .equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)) {
+ return false;
+ }
+
+ try {
+ context.startActivityAsUser(forwardCallIntent, initiatingUser);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ Log.e(CallIntentProcessor.class, e, "Unable to start call intent in the main user");
+ return false;
+ }
+ }
}
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 1f87593..4484e23 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -254,12 +254,11 @@
return false;
}
- PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
- if (b == null) {
+ if (mCarrierConfigManager == null) {
return false;
}
-
- if (b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
+ PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
+ if (b == null || b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
return false;
}
}
@@ -369,7 +368,7 @@
if (phoneAccount != null &&
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
if (initiatingUser != null &&
- UserUtil.isManagedProfile(mContext, initiatingUser)) {
+ UserUtil.isProfile(mContext, initiatingUser, mFeatureFlags)) {
paramBuilder.setUserToBeInsertedTo(initiatingUser);
paramBuilder.setAddForAllUsers(false);
} else {
@@ -575,17 +574,10 @@
AddCallArgs c = callList[i];
mListeners[i] = c.logCallCompletedListener;
try {
- 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) {
+ Log.i(TAG, "LogCall; logged callId=%s, uri=%s",
+ c.call.getId(), result[i]);
+ if (result[i] == null) {
// 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
@@ -686,52 +678,6 @@
}
}
- /**
- * 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/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
new file mode 100644
index 0000000..d579542
--- /dev/null
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.CallEndpoint;
+
+import java.util.Set;
+
+/**
+ * android.telecom.Call backed Services (i.e. ConnectionService, TransactionalService, etc.) that
+ * have callbacks that can be executed before the service is set (within the Call object) should
+ * implement this interface in order for clients to receive the callback.
+ *
+ * It has been shown that clients can miss important callback information (e.g. available audio
+ * endpoints) if the service is null within the call at the time the callback is sent. This is a
+ * way to eliminate the timing issue and for clients to receive all callbacks.
+ */
+public interface CallSourceService {
+ void onMuteStateChanged(Call activeCall, boolean isMuted);
+
+ void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint);
+
+ void onAvailableCallEndpointsChanged(Call activeCall, Set<CallEndpoint> availableCallEndpoints);
+
+ void onVideoStateChanged(Call activeCall, int videoState);
+}
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 1323633..efd458e 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -127,7 +127,7 @@
if (mCallsManager.getCallStreamingController().isStreaming()) {
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_ALREADY_STREAMING"));
} else {
future.complete(new VoipCallTransactionResult(
@@ -196,7 +196,8 @@
if (roleManager == null || packageManager == null) {
Log.w(this, "processTransaction: Can't find system service");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
@@ -205,7 +206,8 @@
if (holders.isEmpty()) {
Log.w(this, "processTransaction: Can't find streaming app");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
@@ -216,7 +218,8 @@
if (infos.isEmpty()) {
Log.w(this, "processTransaction: Can't find streaming service");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
@@ -227,7 +230,8 @@
Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
serviceInfo.packageName);
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
@@ -239,7 +243,7 @@
| Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
Log.w(this, "Can't bind to streaming service");
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
}
return future;
@@ -379,7 +383,8 @@
VoipCallTransactionResult.RESULT_SUCCEED, null));
} catch (RemoteException e) {
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, "Exception when request "
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ "Exception when request "
+ "setting state to streaming app."));
}
return future;
@@ -409,7 +414,7 @@
} catch (RemoteException e) {
resetController();
mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
StreamingServiceTransaction.MESSAGE));
}
}
@@ -433,7 +438,7 @@
resetController();
if (!mFuture.isDone()) {
mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
} else {
mWrapper.onCallStreamingFailed(mCall, STREAMING_FAILED_SENDER_BINDING_ERROR);
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index afe201b..600f847 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -79,7 +79,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.BlockedNumberContract;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumbersManager;
import android.provider.CallLog.Calls;
import android.provider.Settings;
import android.telecom.CallAttributes;
@@ -488,6 +488,7 @@
private final TransactionManager mTransactionManager;
private final UserManager mUserManager;
private final CallStreamingNotification mCallStreamingNotification;
+ private final BlockedNumbersManager mBlockedNumbersManager;
private final FeatureFlags mFeatureFlags;
private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
@@ -560,7 +561,8 @@
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
- || BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
+ || BlockedNumbersManager
+ .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
updateEmergencyCallNotificationAsync(context);
}
}
@@ -656,10 +658,12 @@
);
} else {
callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
- new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
+ new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
+ statusBarNotifier, featureFlags);
}
callAudioRouteAdapter.initialize();
bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+ bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
new CallAudioRoutePeripheralAdapter(
@@ -679,7 +683,7 @@
() -> audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0);
SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
- RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
+ RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context, featureFlags);
SystemVibrator systemVibrator = new SystemVibrator(context);
mInCallController = inCallControllerFactory.create(context, mLock, this,
systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
@@ -722,6 +726,9 @@
mCallStreamingNotification = callStreamingNotification;
mFeatureFlags = featureFlags;
mTelephonyFeatureFlags = telephonyFlags;
+ mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+ ? mContext.getSystemService(BlockedNumbersManager.class)
+ : null;
if (mFeatureFlags.useImprovedListenerOrder()) {
mListeners.add(mInCallController);
@@ -753,7 +760,7 @@
mVoipCallMonitor.startMonitor();
// There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
- final UserManager userManager = UserManager.get(mContext);
+ final UserManager userManager = mContext.getSystemService(UserManager.class);
// Don't load missed call if it is run in split user model.
if (userManager.isPrimaryUser()) {
onUserSwitch(Process.myUserHandle());
@@ -762,7 +769,7 @@
IntentFilter intentFilter = new IntentFilter(
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
- intentFilter.addAction(BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+ intentFilter.addAction(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
mGraphHandlerThreads = new LinkedList<>();
@@ -855,10 +862,16 @@
? new Bundle()
: phoneAccount.getExtras();
TelephonyManager telephonyManager = getTelephonyManager();
+ boolean isInEmergencySmsMode;
+ try {
+ isInEmergencySmsMode = telephonyManager.isInEmergencySmsMode();
+ } catch (UnsupportedOperationException uoe) {
+ isInEmergencySmsMode = false;
+ }
boolean performDndFilter = mFeatureFlags.skipFilterPhoneAccountPerformDndFilter();
if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) ||
incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL) ||
- telephonyManager.isInEmergencySmsMode() ||
+ isInEmergencySmsMode ||
incomingCall.isSelfManaged() ||
(!performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING))) {
Log.i(this, "Skipping call filtering for %s (ecm=%b, "
@@ -867,7 +880,7 @@
incomingCall.getId(),
incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE),
incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL),
- telephonyManager.isInEmergencySmsMode(),
+ isInEmergencySmsMode,
incomingCall.isSelfManaged(),
extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING));
onCallFilteringComplete(incomingCall, new Builder()
@@ -916,7 +929,7 @@
DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
mCallerInfoLookupHelper);
BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
- mCallerInfoLookupHelper, new BlockCheckerAdapter());
+ mCallerInfoLookupHelper, new BlockCheckerAdapter(mFeatureFlags));
DndCallFilter dndCallFilter = new DndCallFilter(incomingCall, getRinger());
CallScreeningServiceFilter carrierCallScreeningServiceFilter =
new CallScreeningServiceFilter(incomingCall, carrierPackageName,
@@ -951,6 +964,7 @@
ComponentName componentName = null;
CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService
(Context.CARRIER_CONFIG_SERVICE);
+ if (configManager == null) return null;
PersistableBundle configBundle = configManager.getConfig();
if (configBundle != null) {
componentName = ComponentName.unflattenFromString(configBundle.getString
@@ -1025,7 +1039,8 @@
if (result.shouldAllowCall) {
if (mFeatureFlags.separatelyBindToBtIncallService()) {
- incomingCall.setBtIcsFuture(mInCallController.bindToBTService(incomingCall));
+ mInCallController.bindToBTService(incomingCall, null);
+ incomingCall.setBtIcsFuture(mInCallController.getBtBindingFuture(incomingCall));
setCallState(incomingCall, CallState.RINGING, "successful incoming call");
}
incomingCall.setPostCallPackageName(
@@ -1404,7 +1419,7 @@
return mCallEndpointController;
}
- EmergencyCallHelper getEmergencyCallHelper() {
+ public EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
@@ -1681,9 +1696,15 @@
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());
+ UserManager currentUserManager = mContext.createContextAsUser(mCurrentUserHandle, 0)
+ .getSystemService(UserManager.class);
+ boolean isCurrentUserAdmin = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? currentUserManager.isAdminUser()
+ : mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier());
+ if (isCurrentUserAdmin) {
+ isCallHiddenFromProfile &= mFeatureFlags.telecomResolveHiddenDependencies()
+ ? currentUserManager.isQuietModeEnabled(call.getAssociatedUser())
+ : mUserManager.isQuietModeEnabled(call.getAssociatedUser());
}
// We should always allow emergency calls and also allow non-emergency calls when ECBM
@@ -1906,19 +1927,25 @@
// Log info for emergency call
if (call.isEmergencyCall()) {
- String simNumeric = "";
- String networkNumeric = "";
- int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
- if (defaultVoiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
- TelephonyManager tm = getTelephonyManager().createForSubscriptionId(
- defaultVoiceSubId);
- CellIdentity cellIdentity = tm.getLastKnownCellIdentity();
- simNumeric = tm.getSimOperatorNumeric();
- networkNumeric = (cellIdentity != null) ? cellIdentity.getPlmn() : "";
- }
- TelecomStatsLog.write(TelecomStatsLog.EMERGENCY_NUMBER_DIALED,
+ try {
+ String simNumeric = "";
+ String networkNumeric = "";
+ int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
+ if (defaultVoiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ TelephonyManager tm = getTelephonyManager().createForSubscriptionId(
+ defaultVoiceSubId);
+ CellIdentity cellIdentity = tm.getLastKnownCellIdentity();
+ simNumeric = tm.getSimOperatorNumeric();
+ networkNumeric = (cellIdentity != null) ? cellIdentity.getPlmn() : "";
+ }
+ TelecomStatsLog.write(TelecomStatsLog.EMERGENCY_NUMBER_DIALED,
handle.getSchemeSpecificPart(),
callingPackage, simNumeric, networkNumeric);
+ } catch (UnsupportedOperationException uoe) {
+ // Ignore; likely we should not be able to get here since emergency calls
+ // require Telephony at the current time, however that could change in the
+ // future, so we best be safe.
+ }
}
// Ensure new calls related to self-managed calls/connections are set as such. This
@@ -2140,7 +2167,7 @@
Uri callUri = callToPlace.getHandle();
if (PhoneAccount.SCHEME_TEL.equals(callUri.getScheme())) {
int managedProfileUserId = getManagedProfileUserId(mContext,
- initiatingUser.getIdentifier());
+ initiatingUser.getIdentifier(), mFeatureFlags);
if (managedProfileUserId != UserHandle.USER_NULL
&&
mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
@@ -2180,11 +2207,16 @@
// 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);
+ // SubscriptionManager will throw if FEATURE_TELEPHONY_SUBSCRIPTION
+ // is not present.
+ if (mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+ 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);
+ }
}
}
@@ -2315,15 +2347,35 @@
return mLatestPostSelectionProcessingFuture;
}
- private static int getManagedProfileUserId(Context context, int userId) {
- UserManager um = context.getSystemService(UserManager.class);
- List<UserInfo> userProfiles = um.getProfiles(userId);
- for (UserInfo uInfo : userProfiles) {
- if (uInfo.id == userId) {
- continue;
+ private static int getManagedProfileUserId(Context context, int userId,
+ FeatureFlags featureFlags) {
+ UserManager um;
+ UserHandle userHandle = UserHandle.of(userId);
+ um = featureFlags.telecomResolveHiddenDependencies()
+ ? context.createContextAsUser(userHandle, 0).getSystemService(UserManager.class)
+ : context.getSystemService(UserManager.class);
+
+ if (featureFlags.telecomResolveHiddenDependencies()) {
+ List<UserHandle> userProfiles = um.getAllProfiles();
+ for (UserHandle userProfile : userProfiles) {
+ UserManager profileUserManager = context.createContextAsUser(userProfile, 0)
+ .getSystemService(UserManager.class);
+ if (userProfile.getIdentifier() == userId) {
+ continue;
+ }
+ if (profileUserManager.isManagedProfile()) {
+ return userProfile.getIdentifier();
+ }
}
- if (uInfo.isManagedProfile()) {
- return uInfo.id;
+ } else {
+ List<UserInfo> userInfoProfiles = um.getProfiles(userId);
+ for (UserInfo uInfo : userInfoProfiles) {
+ if (uInfo.id == userId) {
+ continue;
+ }
+ if (uInfo.isManagedProfile()) {
+ return uInfo.id;
+ }
}
}
return UserHandle.USER_NULL;
@@ -2436,8 +2488,8 @@
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())) {
+ if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser, null,
+ isSelfManaged, CallsManager.class.getCanonicalName(), mFeatureFlags)) {
return;
}
CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle,
@@ -2648,6 +2700,9 @@
isEmergencyNumber =
handle != null && getTelephonyManager().isEmergencyNumber(
handle.getSchemeSpecificPart());
+ } catch (UnsupportedOperationException uoe) {
+ // If device has no telephony, we can't check if it is an emergency call.
+ isEmergencyNumber = false;
} catch (IllegalStateException ise) {
isEmergencyNumber = false;
} catch (RuntimeException r) {
@@ -2936,9 +2991,13 @@
}
if (call.isEmergencyCall()) {
- Executors.defaultThreadFactory().newThread(() ->
- BlockedNumberContract.BlockedNumbers.notifyEmergencyContact(mContext))
- .start();
+ Executors.defaultThreadFactory().newThread(() -> {
+ if (mBlockedNumbersManager != null) {
+ mBlockedNumbersManager.notifyEmergencyContact();
+ } else {
+ BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext);
+ }
+ }).start();
}
final boolean requireCallCapableAccountByHandle = mContext.getResources().getBoolean(
@@ -3444,11 +3503,15 @@
}
// Returns whether the device is capable of 2 simultaneous active voice calls on different subs.
- private boolean isDsdaCallingPossible() {
+ @VisibleForTesting
+ public boolean isDsdaCallingPossible() {
try {
return getTelephonyManager().getMaxNumberOfSimultaneouslyActiveSims() > 1
|| getTelephonyManager().getPhoneCapability()
.getMaxActiveVoiceSubscriptions() > 1;
+ } catch(UnsupportedOperationException uoe) {
+ Log.w(this, "Telephony not supported");
+ return false;
} catch (Exception e) {
Log.w(this, "exception in isDsdaCallingPossible(): ", e);
return false;
@@ -3674,6 +3737,7 @@
int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
CarrierConfigManager carrierConfigManager =
mContext.getSystemService(CarrierConfigManager.class);
+ if (carrierConfigManager == null) return new PersistableBundle();
PersistableBundle result = carrierConfigManager.getConfigForSubId(subscriptionId);
return result == null ? new PersistableBundle() : result;
}
@@ -3772,8 +3836,7 @@
if (canHold(activeCall)) {
activeCall.hold("swap to " + call.getId());
return true;
- } else if (supportsHold(activeCall)
- && areFromSameSource(activeCall, call)) {
+ } else if (sameSourceHoldCase(activeCall, call)) {
// Handle the case where the active call and the new call are from the same CS or
// connection manager, and the currently active call supports hold but cannot
@@ -3822,43 +3885,85 @@
return false;
}
- // attempt to hold the requested call and complete the callback on the result
+ /**
+ * attempt to hold or swap the current active call in favor of a new call request. The
+ * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+ * Otherwise, the OutcomeReceiver will fail.
+ */
public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
- OutcomeReceiver<Boolean, CallException> callback) {
+ boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+ String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+ Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
- // early exit if there is no need to hold an active call
if (activeCall == null || activeCall == newCall) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall:"
- + " no need to hold activeCall");
+ Log.i(this, mTag + "no need to hold activeCall");
callback.onResult(true);
return;
}
- // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail early
- if (!canHold(activeCall) &&
- !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "conditions show the call cannot be held.");
- callback.onError(new CallException("call does not support hold",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
+ if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+ // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+ // notify the user that they need to disconnect the ongoing call before making the
+ // new call ACTIVE.
+ if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
+ Log.i(this, mTag + "CallControlRequest exit");
+ callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+ + " request the user disconnect the call.",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
- // attempt to hold the active call
- if (!holdActiveCallForNewCall(newCall)) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "attempted to hold call but failed.");
- callback.onError(new CallException("cannot hold active call failed",
- CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
- return;
- }
+ if (holdActiveCallForNewCall(newCall)) {
+ // Transactional clients do not call setHold but the request was sent to set the
+ // call as inactive and it has already been acked by this point.
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ } else {
+ // It's possible that holdActiveCallForNewCall disconnected the activeCall.
+ // Therefore, the activeCalls state should be checked before failing.
+ if (activeCall.isLocallyDisconnecting()) {
+ callback.onResult(true);
+ } else {
+ Log.i(this, mTag + "active call could not be held or disconnected");
+ callback.onError(
+ new CallException("activeCall could not be held or disconnected",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ }
+ }
+ } else {
+ // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+ // early
+ if (!canHold(activeCall) &&
+ !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "conditions show the call cannot be held.");
+ callback.onError(new CallException("call does not support hold",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
- // officially mark the activeCall as held
- markCallAsOnHold(activeCall);
- callback.onResult(true);
+ // attempt to hold the active call
+ if (!holdActiveCallForNewCall(newCall)) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "attempted to hold call but failed.");
+ callback.onError(new CallException("cannot hold active call failed",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
+
+ // officially mark the activeCall as held
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ }
+ }
+
+ private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+ return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
+ }
+
+ private boolean sameSourceHoldCase(Call activeCall, Call call) {
+ return supportsHold(activeCall) && areFromSameSource(activeCall, call);
}
@VisibleForTesting
@@ -3952,20 +4057,21 @@
Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);
// Setup the future with a timeout so that the CDS is time boxed.
- CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
+ CompletableFuture<Boolean> future = call.initializeDiagnosticCompleteFuture(
mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
mContext.getContentResolver()));
// Post the disconnection updates to the future for completion once the CDS returns
// with it's overridden disconnect message.
- future.thenRunAsync(() -> {
+ CompletableFuture<Void> disconnectFuture = future.thenRunAsync(() -> {
call.setDisconnectCause(disconnectCause);
setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
- }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
- .exceptionally((throwable) -> {
- Log.e(TAG, throwable, "Error while executing disconnect future.");
- return null;
- });
+ }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock));
+ disconnectFuture.exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future.");
+ return null;
+ });
+ call.setDisconnectFuture(disconnectFuture);
} else {
// No CallDiagnosticService, or it doesn't handle this call, so just do this
// synchronously as always.
@@ -3985,16 +4091,7 @@
public void markCallAsRemoved(Call call) {
if (call.isDisconnectHandledViaFuture()) {
Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
- // A future is being used due to a CallDiagnosticService handling the call. We will
- // chain the removal operation to the end of any outstanding disconnect work.
- call.getDisconnectFuture().thenRunAsync(() -> {
- performRemoval(call);
- }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
- .exceptionally((throwable) -> {
- Log.e(TAG, throwable, "Error while executing disconnect future");
- return null;
- });
-
+ configureRemovalFuture(call);
} else {
Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
performRemoval(call);
@@ -4002,8 +4099,52 @@
}
/**
+ * Configure the removal as a dependent stage after the disconnect future completes, which could
+ * be cancelled as part of {@link Call#setState(int, String)} when need to retry dial on another
+ * ConnectionService.
+ * <p>
+ * We can not remove the call yet, we need to wait for the DisconnectCause to be processed and
+ * potentially re-written via the {@link android.telecom.CallDiagnosticService} first.
+ *
+ * @param call The call to configure the removal future for.
+ */
+ private void configureRemovalFuture(Call call) {
+ if (!mFeatureFlags.cancelRemovalOnEmergencyRedial()) {
+ call.getDiagnosticCompleteFuture().thenRunAsync(() -> performRemoval(call),
+ new LoggedHandlerExecutor(mHandler, "CM.cRF-O", mLock))
+ .exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future");
+ return null;
+ });
+ } else {
+ // A future is being used due to a CallDiagnosticService handling the call. We will
+ // chain the removal operation to the end of any outstanding disconnect work.
+ CompletableFuture<Void> removalFuture;
+ if (call.getDisconnectFuture() == null) {
+ // Unexpected - can not get the disconnect future, attach to the diagnostic complete
+ // future in this case.
+ removalFuture = call.getDiagnosticCompleteFuture().thenRun(() ->
+ Log.w(this, "configureRemovalFuture: remove called without disconnecting"
+ + " first."));
+ } else {
+ removalFuture = call.getDisconnectFuture();
+ }
+ removalFuture = removalFuture.thenRunAsync(() -> performRemoval(call),
+ new LoggedHandlerExecutor(mHandler, "CM.cRF-N", mLock));
+ removalFuture.exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing disconnect future");
+ return null;
+ });
+ // Cache the future to remove the call initiated by the ConnectionService in case we
+ // need to cancel it in favor of removing the call internally as part of creating a
+ // new connection (CreateConnectionProcessor#continueProcessingIfPossible)
+ call.setRemovalFuture(removalFuture);
+ }
+ }
+
+ /**
* Work which is completed when a call is to be removed. Can either be be run synchronously or
- * posted to a {@link Call#getDisconnectFuture()}.
+ * posted to a {@link Call#getDiagnosticCompleteFuture()}.
* @param call The call.
*/
private void performRemoval(Call call) {
@@ -4778,19 +4919,7 @@
* @return {@code true} if the app has ongoing calls, or {@code false} otherwise.
*/
public boolean isInSelfManagedCall(String packageName, UserHandle userHandle) {
- return isInSelfManagedCallCrossUsers(packageName, userHandle, false);
- }
-
- /**
- * Determines if there are any ongoing self-managed calls for the given package/user (unless
- * hasCrossUsers has been enabled).
- * @param packageName The package name to check.
- * @param userHandle The {@link UserHandle} to check.
- * @param hasCrossUserAccess indicates if calls across all users should be returned.
- * @return {@code true} if the app has ongoing calls, or {@code false} otherwise.
- */
- public boolean isInSelfManagedCallCrossUsers(
- String packageName, UserHandle userHandle, boolean hasCrossUserAccess) {
+ boolean hasCrossUserAccess = userHandle.equals(UserHandle.ALL);
return mSelfManagedCallsBeingSetup.stream().anyMatch(c -> c.isSelfManaged()
&& c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
&& (!hasCrossUserAccess
@@ -5541,10 +5670,21 @@
mCurrentUserHandle = userHandle;
mMissedCallNotifier.setCurrentUserHandle(userHandle);
mRoleManagerAdapter.setCurrentUserHandle(userHandle);
- final UserManager userManager = UserManager.get(mContext);
- List<UserInfo> profiles = userManager.getEnabledProfiles(userHandle.getIdentifier());
- for (UserInfo profile : profiles) {
- reloadMissedCallsOfUser(profile.getUserHandle());
+ final UserManager userManager = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? mContext.createContextAsUser(userHandle, 0).getSystemService(
+ UserManager.class)
+ : mContext.getSystemService(UserManager.class);
+ List<UserHandle> profiles = userManager.getUserProfiles();
+ List<UserInfo> userInfoProfiles = userManager.getEnabledProfiles(
+ userHandle.getIdentifier());
+ if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+ for (UserHandle profileUser : profiles) {
+ reloadMissedCallsOfUser(profileUser);
+ }
+ } else {
+ for (UserInfo profile : userInfoProfiles) {
+ reloadMissedCallsOfUser(profile.getUserHandle());
+ }
}
}
@@ -5553,7 +5693,7 @@
* switched, we reload missed calls of profile that are just started here.
*/
void onUserStarting(UserHandle userHandle) {
- if (UserUtil.isProfile(mContext, userHandle)) {
+ if (UserUtil.isProfile(mContext, userHandle, mFeatureFlags)) {
reloadMissedCallsOfUser(userHandle);
}
}
@@ -5662,8 +5802,10 @@
UserManager userManager = mContext.getSystemService(UserManager.class);
KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
- boolean isUserRestricted = userManager != null
- && userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
+ boolean hasUserRestriction = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? userManager.hasUserRestrictionForUser(UserManager.DISALLOW_SMS, callingUser)
+ : userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
+ boolean isUserRestricted = userManager != null && hasUserRestriction;
boolean isLockscreenRestricted = keyguardManager != null
&& keyguardManager.isDeviceLocked();
Log.d(this, "isReplyWithSmsAllowed: isUserRestricted: %s, isLockscreenRestricted: %s",
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 2ed416d..79ce5a3 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -33,7 +33,6 @@
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;
@@ -45,6 +44,7 @@
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
+import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
@@ -66,7 +66,6 @@
import com.android.internal.telecom.RemoteServiceCallback;
import com.android.internal.util.Preconditions;
import com.android.server.telecom.flags.FeatureFlags;
-import com.android.server.telecom.flags.Flags;
import java.util.ArrayList;
import java.util.Collection;
@@ -75,10 +74,13 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
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.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
@@ -90,13 +92,30 @@
*/
@VisibleForTesting
public class ConnectionServiceWrapper extends ServiceBinder implements
- ConnectionServiceFocusManager.ConnectionServiceFocus {
+ ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
+
+ /**
+ * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+ */
+ public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
+ UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
+ public static final String CREATE_CONNECTION_TIMEOUT_ERROR_MSG =
+ "Timeout expired before Telecom connection was created.";
+ public static final UUID CREATE_CONFERENCE_TIMEOUT_ERROR_UUID =
+ UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
+ public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
+ "Timeout expired before Telecom conference was created.";
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();
+ // Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
+ private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
+ private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
private final class Adapter extends IConnectionServiceAdapter.Stub {
@Override
@@ -109,6 +128,12 @@
try {
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
+ Call call = mCallIdMapper.getCall(callId);
+ if (mScheduledFutureMap.containsKey(call)) {
+ ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+ existingTimeout.cancel(false /* cancelIfRunning */);
+ mScheduledFutureMap.remove(call);
+ }
// Check status hints image for cross user access
if (connection.getStatusHints() != null) {
Icon icon = connection.getStatusHints().getIcon();
@@ -146,6 +171,12 @@
try {
synchronized (mLock) {
logIncoming("handleCreateConferenceComplete %s", callId);
+ Call call = mCallIdMapper.getCall(callId);
+ if (mScheduledFutureMap.containsKey(call)) {
+ ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+ existingTimeout.cancel(false /* cancelIfRunning */);
+ mScheduledFutureMap.remove(call);
+ }
ConnectionServiceWrapper.this
.handleCreateConferenceComplete(callId, request, conference);
@@ -383,7 +414,12 @@
logIncoming("removeCall %s", callId);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
- if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
+ boolean isRemovalPending = mFlags.cancelRemovalOnEmergencyRedial()
+ && call.isRemovalPending();
+ if (call.isAlive() && !call.isDisconnectHandledViaFuture()
+ && !isRemovalPending) {
+ Log.w(this, "call not disconnected when removeCall"
+ + " called, marking disconnected first.");
mCallsManager.markCallAsDisconnected(
call, new DisconnectCause(DisconnectCause.REMOTE));
}
@@ -1409,20 +1445,23 @@
}
}
- private CellIdentity getLastKnownCellIdentity() {
+ @VisibleForTesting
+ public CellIdentity getLastKnownCellIdentity() {
TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
if (telephonyManager != null) {
- CellIdentity lastKnownCellIdentity = telephonyManager.getLastKnownCellIdentity();
try {
+ CellIdentity lastKnownCellIdentity = telephonyManager.getLastKnownCellIdentity();
mAppOpsManager.noteOp(AppOpsManager.OP_FINE_LOCATION,
mContext.getPackageManager().getPackageUid(
getComponentName().getPackageName(), 0),
getComponentName().getPackageName());
+ return lastKnownCellIdentity;
+ } catch (UnsupportedOperationException ignored) {
+ Log.w(this, "getLastKnownCellIdentity - no telephony on this device");
} catch (PackageManager.NameNotFoundException nameNotFoundException) {
Log.e(this, nameNotFoundException, "could not find the package -- %s",
getComponentName().getPackageName());
}
- return lastKnownCellIdentity;
}
return null;
}
@@ -1598,6 +1637,29 @@
.setParticipants(call.getParticipants())
.setIsAdhocConferenceCall(call.isAdhocConferenceCall())
.build();
+ Runnable r = new Runnable("CSW.cC", mLock) {
+ @Override
+ public void loggedRun() {
+ if (!call.isCreateConnectionComplete()) {
+ Log.e(this, new Exception(),
+ "Conference %s creation timeout",
+ getComponentName());
+ Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
+ Log.piiHandle(call.getHandle()) + " via:" +
+ getComponentName().getPackageName());
+ mAnomalyReporter.reportAnomaly(
+ CREATE_CONFERENCE_TIMEOUT_ERROR_UUID,
+ CREATE_CONFERENCE_TIMEOUT_ERROR_MSG);
+ response.handleCreateConferenceFailure(
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+ }
+ };
+ // Post cleanup to the executor service and cache the future, so we can cancel it if
+ // needed.
+ ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+ SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+ mScheduledFutureMap.put(call, future);
try {
mServiceInterface.createConference(
call.getConnectionManagerPhoneAccount(),
@@ -1698,6 +1760,29 @@
.setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
.setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
.build();
+ Runnable r = new Runnable("CSW.cC", mLock) {
+ @Override
+ public void loggedRun() {
+ if (!call.isCreateConnectionComplete()) {
+ Log.e(this, new Exception(),
+ "Connection %s creation timeout",
+ getComponentName());
+ Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
+ Log.piiHandle(call.getHandle()) + " via:" +
+ getComponentName().getPackageName());
+ mAnomalyReporter.reportAnomaly(
+ CREATE_CONNECTION_TIMEOUT_ERROR_UUID,
+ CREATE_CONNECTION_TIMEOUT_ERROR_MSG);
+ response.handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+ }
+ };
+ // Post cleanup to the executor service and cache the future, so we can cancel it if
+ // needed.
+ ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+ SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+ mScheduledFutureMap.put(call, future);
try {
mServiceInterface.createConnection(
call.getConnectionManagerPhoneAccount(),
@@ -1953,6 +2038,7 @@
/** @see IConnectionService#onCallEndpointChanged(String, CallEndpoint, Session.Info) */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Override
public void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint) {
final String callId = mCallIdMapper.getCallId(activeCall);
if (callId != null && isServiceValid("onCallEndpointChanged")) {
@@ -1968,6 +2054,7 @@
/** @see IConnectionService#onAvailableCallEndpointsChanged(String, List, Session.Info) */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Override
public void onAvailableCallEndpointsChanged(Call activeCall,
Set<CallEndpoint> availableCallEndpoints) {
final String callId = mCallIdMapper.getCallId(activeCall);
@@ -1984,8 +2071,14 @@
}
}
+ @Override
+ public void onVideoStateChanged(Call call, int videoState){
+ // pass through. ConnectionService does not implement this method.
+ }
+
/** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Override
public void onMuteStateChanged(Call activeCall, boolean isMuted) {
final String callId = mCallIdMapper.getCallId(activeCall);
if (callId != null && isServiceValid("onMuteStateChanged")) {
@@ -2028,7 +2121,8 @@
}
/** @see IConnectionService#disconnect(String, Session.Info) */
- void disconnect(Call call) {
+ @VisibleForTesting
+ public void disconnect(Call call) {
final String callId = mCallIdMapper.getCallId(call);
if (callId != null && isServiceValid("disconnect")) {
try {
@@ -2155,7 +2249,8 @@
}
}
- void addCall(Call call) {
+ @VisibleForTesting
+ public void addCall(Call call) {
if (mCallIdMapper.getCallId(call) == null) {
mCallIdMapper.addCall(call);
}
@@ -2623,4 +2718,14 @@
sb.append("]");
return sb.toString();
}
+
+ @VisibleForTesting
+ public void setScheduledExecutorService(ScheduledExecutorService service) {
+ mScheduledExecutor = service;
+ }
+
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+ mAnomalyReporter = mAnomalyReporterAdapter;
+ }
}
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index bcb2d2f..a2c742d 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -103,22 +103,32 @@
int getSlotIndex(int subId);
}
- private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapter() {
+ public static class ITelephonyManagerAdapterImpl implements ITelephonyManagerAdapter {
@Override
public int getSubIdForPhoneAccount(Context context, PhoneAccount account) {
TelephonyManager manager = context.getSystemService(TelephonyManager.class);
if (manager == null) {
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
- return manager.getSubscriptionId(account.getAccountHandle());
+ try {
+ return manager.getSubscriptionId(account.getAccountHandle());
+ } catch (UnsupportedOperationException uoe) {
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
}
@Override
public int getSlotIndex(int subId) {
- return SubscriptionManager.getSlotIndex(subId);
+ try {
+ return SubscriptionManager.getSlotIndex(subId);
+ } catch (UnsupportedOperationException uoe) {
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
}
};
+ private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapterImpl();
+
private final Call mCall;
private final ConnectionServiceRepository mRepository;
private List<CallAttemptRecord> mAttemptRecords;
@@ -128,15 +138,19 @@
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final Context mContext;
private final FeatureFlags mFlags;
+ private final Timeouts.Adapter mTimeoutsAdapter;
private CreateConnectionTimeout mTimeout;
private ConnectionServiceWrapper mService;
private int mConnectionAttempt;
@VisibleForTesting
- public CreateConnectionProcessor(
- Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
- PhoneAccountRegistrar phoneAccountRegistrar, Context context,
- FeatureFlags featureFlags) {
+ public CreateConnectionProcessor(Call call,
+ ConnectionServiceRepository repository,
+ CreateConnectionResponse response,
+ PhoneAccountRegistrar phoneAccountRegistrar,
+ Context context,
+ FeatureFlags featureFlags,
+ Timeouts.Adapter timeoutsAdapter) {
Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
mCall = call;
mRepository = repository;
@@ -145,6 +159,7 @@
mContext = context;
mConnectionAttempt = 0;
mFlags = featureFlags;
+ mTimeoutsAdapter = timeoutsAdapter;
}
boolean isProcessingComplete() {
@@ -317,7 +332,7 @@
clearTimeout();
CreateConnectionTimeout timeout = new CreateConnectionTimeout(
- mContext, mPhoneAccountRegistrar, service, mCall);
+ mContext, mPhoneAccountRegistrar, service, mCall, mTimeoutsAdapter);
if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
attempt.connectionManagerPhoneAccount)) {
mTimeout = timeout;
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 14e5bf0..3046ca4 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -16,21 +16,27 @@
package com.android.server.telecom;
+import static com.android.internal.telephony.flags.Flags.carrierEnabledSatelliteFlag;
+
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
+import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.Collection;
import java.util.Objects;
/**
* Registers a timeout for a call and disconnects the call when the timeout expires.
*/
-final class CreateConnectionTimeout extends Runnable {
+@VisibleForTesting
+public final class CreateConnectionTimeout extends Runnable {
private final Context mContext;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final ConnectionServiceWrapper mConnectionService;
@@ -38,20 +44,25 @@
private final Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mIsRegistered;
private boolean mIsCallTimedOut;
+ private final Timeouts.Adapter mTimeoutsAdapter;
- CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
- ConnectionServiceWrapper service, Call call) {
+ @VisibleForTesting
+ public CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
+ ConnectionServiceWrapper service, Call call, Timeouts.Adapter timeoutsAdapter) {
super("CCT", null /*lock*/);
mContext = context;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mConnectionService = service;
mCall = call;
+ mTimeoutsAdapter = timeoutsAdapter;
}
- boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
+ @VisibleForTesting
+ public boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
PhoneAccountHandle currentAccount) {
// Non-emergency calls timeout automatically at the radio layer. No need for a timeout here.
if (!mCall.isEmergencyCall()) {
+ Log.d(this, "isTimeoutNeededForCall, not an emergency call");
return false;
}
@@ -60,11 +71,13 @@
PhoneAccountHandle connectionManager =
mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
if (!accounts.contains(connectionManager)) {
+ Log.d(this, "isTimeoutNeededForCall, no connection manager");
return false;
}
// No need to add a timeout if the current attempt is over the connection manager.
if (Objects.equals(connectionManager, currentAccount)) {
+ Log.d(this, "isTimeoutNeededForCall, already attempting over connection manager");
return false;
}
@@ -104,8 +117,34 @@
@Override
public void loggedRun() {
+ if (!carrierEnabledSatelliteFlag()) {
+ timeoutCallIfNeeded();
+ return;
+ }
+
+ PhoneAccountHandle connectionManager =
+ mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
+ if (connectionManager != null) {
+ PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(connectionManager,
+ connectionManager.getUserHandle());
+ if (account != null && account.hasCapabilities(
+ (PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+ | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE))) {
+ // If we have encountered the timeout and there is an in service
+ // ConnectionManager, disconnect the call so that it can be attempted over
+ // the ConnectionManager.
+ timeoutCallIfNeeded();
+ return;
+ }
+ Log.i(
+ this,
+ "loggedRun, no PhoneAccount with voice calling capabilities, not timing out call");
+ }
+ }
+
+ private void timeoutCallIfNeeded() {
if (mIsRegistered && isCallBeingPlaced(mCall)) {
- Log.i(this, "run, call timed out, calling disconnect");
+ Log.i(this, "timeoutCallIfNeeded, call timed out, calling disconnect");
mIsCallTimedOut = true;
mConnectionService.disconnect(mCall);
}
@@ -122,13 +161,19 @@
private long getTimeoutLengthMillis() {
// If the radio is off then use a longer timeout. This gives us more time to power on the
// radio.
- TelephonyManager telephonyManager =
- (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
- if (telephonyManager.isRadioOn()) {
- return Timeouts.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
- } else {
- return Timeouts.getEmergencyCallTimeoutRadioOffMillis(
- mContext.getContentResolver());
+ try {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager.isRadioOn()) {
+ return mTimeoutsAdapter.getEmergencyCallTimeoutMillis(
+ mContext.getContentResolver());
+ } else {
+ return mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(
+ mContext.getContentResolver());
+ }
+ } catch (UnsupportedOperationException uoe) {
+ Log.e(this, uoe, "getTimeoutLengthMillis - telephony is not supported");
+ return mTimeoutsAdapter.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
}
}
}
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index d819780..44b426a 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -176,7 +176,7 @@
UserHandle.USER_ALL);
}
- public String getBTInCallServicePackage() {
+ public String[] getBTInCallServicePackages() {
return mRoleManagerAdapter.getBTInCallService();
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index f464d04..f3c84ba 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -361,7 +361,8 @@
Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
mIsConnected = true;
mInCallServiceInfo.setBindingStartTime(mClockProxy.elapsedRealtime());
- boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userFromCall);
+ boolean isManagedProfile = UserUtil.isManagedProfile(mContext,
+ userFromCall, mFeatureFlags);
// Note that UserHandle.CURRENT fails to capture the work profile, so we need to handle
// it separately to ensure that the ICS is bound to the appropriate user. If ECBM is
// active, we know that a work sim was previously used to place a MO emergency call. We
@@ -369,7 +370,8 @@
// not be running (handled in getUserFromCall).
UserHandle userToBind = isManagedProfile ? userFromCall : UserHandle.CURRENT;
if ((mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_NON_UI
- || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI) && (
+ || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+ || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_BLUETOOTH) && (
mUserHandleToUseForBinding != null)) {
//guarding change for non-UI/carmode-UI services which may not be present for
// work profile.
@@ -1079,6 +1081,7 @@
if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
synchronized (mLock) {
int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+ String changedPackage = intent.getData().getSchemeSpecificPart();
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
boolean isManagedProfile = um.isManagedProfile(userHandle.getIdentifier());
@@ -1108,12 +1111,36 @@
childManagedProfileUser);
List<InCallServiceBindingConnection> componentsToBindForUser = null;
List<InCallServiceBindingConnection> componentsToBindForChild = null;
+ // Separate binding for BT logic.
+ boolean isBluetoothPkg = isBluetoothPackage(changedPackage);
+ Call callToConnectWith = mCallIdMapper.getCalls().isEmpty()
+ ? null
+ : mCallIdMapper.getCalls().iterator().next();
+
+ // Bind to BT service if there's an available call. When the flag isn't
+ // enabled, the service will be included as part of
+ // getNonUiInCallServiceBindingConnectionList.
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && isBluetoothPkg && callToConnectWith != null) {
+ // mNonUIInCallServiceConnections will always contain a key for
+ // userHandle and/or the child user if there is an ongoing call with
+ // that user, regardless if there aren't any non-UI ICS bound.
+ if (isUserKeyPresent) {
+ bindToBTService(callToConnectWith, userHandle);
+ }
+ if (isChildUserKeyPresent) {
+ // This will try to use the ICS found in the parent if one isn't
+ // available for the child.
+ bindToBTService(callToConnectWith, childManagedProfileUser);
+ }
+ }
if(isUserKeyPresent) {
componentsToBindForUser =
getNonUiInCallServiceBindingConnectionList(intent,
userHandle, null);
}
+
if (isChildUserKeyPresent) {
componentsToBindForChild =
getNonUiInCallServiceBindingConnectionList(intent,
@@ -1126,11 +1153,11 @@
isUserKeyPresent, isChildUserKeyPresent, isManagedProfile,
userHandle.getIdentifier());
- if (isUserKeyPresent && componentsToBindForUser != null) {
+ if (isUserKeyPresent && !componentsToBindForUser.isEmpty()) {
mNonUIInCallServiceConnections.get(userHandle).
addConnections(componentsToBindForUser);
}
- if (isChildUserKeyPresent && componentsToBindForChild != null) {
+ if (isChildUserKeyPresent && !componentsToBindForChild.isEmpty()) {
mNonUIInCallServiceConnections.get(childManagedProfileUser).
addConnections(componentsToBindForChild);
}
@@ -1185,6 +1212,10 @@
private static final int IN_CALL_SERVICE_TYPE_COMPANION = 5;
private static final int IN_CALL_SERVICE_TYPE_BLUETOOTH = 6;
+ // Timeout value to be used to ensure future completion for mDisconnectedToneBtFutures. This is
+ // set to 4 seconds to account for the exceptional case (TONE_CONGESTION).
+ private static final int DISCONNECTED_TONE_TIMEOUT = 4000;
+
private static final int[] LIVE_CALL_STATES = { CallState.ACTIVE, CallState.PULLING,
CallState.DISCONNECTING };
@@ -1232,7 +1263,10 @@
// in-call service.
// The future will complete with true if bluetooth in-call service succeeds, false if it timed
// out.
- private CompletableFuture<Boolean> mBtBindingFuture = CompletableFuture.completedFuture(true);
+ private Map<UserHandle, CompletableFuture<Boolean>> mBtBindingFuture = new ArrayMap<>();
+ // Future used to delay terminating the BT InCallService before the call disconnect tone
+ // finishes playing.
+ private Map<String, CompletableFuture<Void>> mDisconnectedToneBtFutures = new ArrayMap<>();
private final CarModeTracker mCarModeTracker;
@@ -1255,6 +1289,8 @@
private boolean mIsStartCallDelayScheduled = false;
+ private boolean mDisconnectedToneStartedPlaying = false;
+
/**
* A list of call IDs which are currently using the camera.
*/
@@ -1374,27 +1410,29 @@
addCall(call);
if (mFeatureFlags.separatelyBindToBtIncallService()) {
- boolean bindBTService = false;
- boolean bindOtherServices = false;
+ boolean bindingToBtRequired = false;
+ boolean bindingToOtherServicesRequired = false;
if (!isBoundAndConnectedToBTService(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected to BT ICS.", call);
- bindBTService = true;
- bindToBTService(call);
+ bindingToBtRequired = true;
+ bindToBTService(call, null);
}
if (!isBoundAndConnectedToServices(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected to other ICS.", call);
// We are not bound, or we're not connected.
- bindOtherServices = true;
- bindToOtherServices(call);
+ bindingToOtherServicesRequired = true;
+ bindToServices(call);
}
- if (!bindBTService || !bindOtherServices) {
+ // If either BT service are already bound or other services are already bound, attempt
+ // to add the new call to the connected incall services.
+ if (!bindingToBtRequired || !bindingToOtherServicesRequired) {
addCallToConnectedServices(call, userFromCall);
}
} else {
if (!isBoundAndConnectedToServices(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected.", call);
// We are not bound, or we're not connected.
- bindToServices(call, false);
+ bindToServices(call);
} else {
addCallToConnectedServices(call, userFromCall);
}
@@ -1506,17 +1544,31 @@
@Override
public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
Log.i(this, "onDisconnectedTonePlaying: %s -> %b", call, isTonePlaying);
-
if (mFeatureFlags.separatelyBindToBtIncallService()) {
synchronized (mLock) {
- mPendingEndToneCall.remove(call);
- if (!mPendingEndToneCall.isEmpty()) {
- return;
- }
- UserHandle userHandle = getUserFromCall(call);
- if (mBTInCallServiceConnections.containsKey(userHandle)) {
- mBTInCallServiceConnections.get(userHandle).disconnect();
- mBTInCallServiceConnections.remove(userHandle);
+ if (isTonePlaying) {
+ mDisconnectedToneStartedPlaying = true;
+ } else if (mDisconnectedToneStartedPlaying) {
+ mDisconnectedToneStartedPlaying = false;
+ if (mDisconnectedToneBtFutures.containsKey(call.getId())) {
+ Log.i(this, "onDisconnectedTonePlaying: completing BT "
+ + "disconnected tone future");
+ mDisconnectedToneBtFutures.get(call.getId()).complete(null);
+ }
+ mPendingEndToneCall.remove(call);
+ if (!mPendingEndToneCall.isEmpty()) {
+ return;
+ }
+ UserHandle userHandle = getUserFromCall(call);
+ if (mBTInCallServiceConnections.containsKey(userHandle)) {
+ Log.i(this, "onDisconnectedTonePlaying: Unbinding BT service");
+ mBTInCallServiceConnections.get(userHandle).disconnect();
+ mBTInCallServiceConnections.remove(userHandle);
+ }
+ // Ensure that BT ICS instance is cleaned up
+ if (mBTInCallServices.remove(userHandle) != null) {
+ updateCombinedInCallServiceMap(userHandle);
+ }
}
}
}
@@ -1968,6 +2020,8 @@
}
getCombinedInCallServiceMap().remove(userHandle);
if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ // Note that the BT ICS will be repopulated as part of the combined map if the
+ // BT ICS is still bound (disconnected tone hasn't finished playing).
updateCombinedInCallServiceMap(userHandle);
}
}
@@ -1978,34 +2032,49 @@
*
* @param call The newly added call that triggered the binding to the in-call services.
*/
- public CompletableFuture<Boolean> bindToBTService(Call call) {
+ public void bindToBTService(Call call, UserHandle userHandle) {
+ Log.i(this, "bindToBtService");
+ UserHandle userToBind = userHandle == null
+ ? getUserFromCall(call)
+ : userHandle;
+ UserManager um = mContext.getSystemService(UserManager.class);
+ UserHandle parentUser = mFeatureFlags.profileUserSupport()
+ ? um.getProfileParent(userToBind) : null;
+
+ if (!mFeatureFlags.profileUserSupport()
+ && um.isManagedProfile(userToBind.getIdentifier())) {
+ parentUser = um.getProfileParent(userToBind);
+ }
+
// Track the call if we don't already know about it.
addCall(call);
- UserHandle userFromCall = getUserFromCall(call);
-
- List<InCallServiceInfo> infos = getInCallServiceComponents(userFromCall,
+ List<InCallServiceInfo> infos = getInCallServiceComponents(userToBind,
IN_CALL_SERVICE_TYPE_BLUETOOTH);
+ boolean serviceUnavailableForUser = false;
if (infos.size() == 0 || infos.get(0) == null) {
- Log.w(this, "No available BT service");
- mBtBindingFuture = CompletableFuture.completedFuture(false);
- return mBtBindingFuture;
+ Log.i(this, "No available BT ICS for user (%s). Trying with parent instead.",
+ userToBind);
+ serviceUnavailableForUser = true;
+ // Check if the service is available under the parent user instead.
+ if (parentUser != null) {
+ infos = getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_BLUETOOTH);
+ }
+ if (infos.size() == 0 || infos.get(0) == null) {
+ Log.w(this, "No available BT ICS to bind to for user %s or its parent %s.",
+ userToBind, parentUser);
+ mBtBindingFuture.put(userToBind, CompletableFuture.completedFuture(false));
+ return;
+ }
}
- mBtBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
- mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
- mContext.getContentResolver()), TimeUnit.MILLISECONDS);
- new InCallServiceBindingConnection(infos.get(0)).connect(call);
- return mBtBindingFuture;
- }
- /**
- * Binds to all the UI-providing InCallService as well as system-implemented non-UI
- * InCallServices except BT InCallServices. Method-invoker must check
- * {@link #isBoundAndConnectedToServices(UserHandle)} before invoking.
- *
- * @param call The newly added call that triggered the binding to the in-call services.
- */
- public void bindToOtherServices(Call call) {
- bindToServices(call, true);
+ mBtBindingFuture.put(userToBind, new CompletableFuture<Boolean>().completeOnTimeout(false,
+ mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
+ mContext.getContentResolver()), TimeUnit.MILLISECONDS));
+ InCallServiceBindingConnection btIcsBindingConnection =
+ new InCallServiceBindingConnection(infos.get(0),
+ serviceUnavailableForUser ? parentUser : userToBind);
+ mBTInCallServiceConnections.put(userToBind, btIcsBindingConnection);
+ btIcsBindingConnection.connect(call);
}
/**
@@ -2015,11 +2084,9 @@
*
* @param call The newly added call that triggered the binding to the in-call
* services.
- * @param skipBTServices Boolean variable to specify if the binding to BT InCallService should
- * be skipped
*/
@VisibleForTesting
- public void bindToServices(Call call, boolean skipBTServices) {
+ public void bindToServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
UserManager um = mContext.getSystemService(UserManager.class);
UserHandle parentUser = mFeatureFlags.profileUserSupport()
@@ -2084,7 +2151,7 @@
// Only connect to the non-ui InCallServices if we actually connected to the main UI
// one, or if the call is self-managed (in which case we'd still want to keep Wear, BT,
// etc. informed.
- connectToNonUiInCallServices(call, skipBTServices);
+ connectToNonUiInCallServices(call);
mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()),
@@ -2099,7 +2166,7 @@
packageChangedFilter, null, null);
}
- private void updateNonUiInCallServices(Call call, boolean skipBTService) {
+ private void updateNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
UserManager um = mContext.getSystemService(UserManager.class);
@@ -2154,10 +2221,10 @@
nonUIInCalls));
}
- private void connectToNonUiInCallServices(Call call, boolean skipBTService) {
+ private void connectToNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
if (!mNonUIInCallServiceConnections.containsKey(userFromCall)) {
- updateNonUiInCallServices(call, skipBTService);
+ updateNonUiInCallServices(call);
}
mNonUIInCallServiceConnections.get(userFromCall).connect(call);
}
@@ -2426,10 +2493,8 @@
return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
}
- String bluetoothPackage = mDefaultDialerCache.getBTInCallServicePackage();
- if (mFeatureFlags.separatelyBindToBtIncallService()
- && serviceInfo.packageName != null
- && serviceInfo.packageName.equals(bluetoothPackage)
+ boolean processingBluetoothPackage = isBluetoothPackage(serviceInfo.packageName);
+ if (mFeatureFlags.separatelyBindToBtIncallService() && processingBluetoothPackage
&& (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
return IN_CALL_SERVICE_TYPE_BLUETOOTH;
}
@@ -2476,11 +2541,13 @@
IInCallService inCallService = IInCallService.Stub.asInterface(service);
if (mFeatureFlags.separatelyBindToBtIncallService()
&& info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
- if (mBtBindingFuture.isDone()) {
+ if (!mBtBindingFuture.containsKey(userHandle)
+ || mBtBindingFuture.get(userHandle).isDone()) {
+ Log.i(this, "onConnected: BT binding future timed out.");
// Binding completed after the timeout. Clean up this binding
return false;
} else {
- mBtBindingFuture.complete(true);
+ mBtBindingFuture.get(userHandle).complete(true);
}
mBTInCallServices.put(userHandle, new Pair<>(info, inCallService));
} else {
@@ -2651,12 +2718,20 @@
IInCallService inCallService = entry.getValue();
componentsUpdated.add(componentName);
- try {
- inCallService.updateCall(
- sanitizeParcelableCallForService(info, parcelableCall));
- } catch (RemoteException exception) {
- Log.w(this, "Call status update did not send to: "
- + componentName +" successfully with error " + exception);
+ if (info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
+ && call.getState() == CallState.DISCONNECTED
+ && !mDisconnectedToneBtFutures.containsKey(call.getId())) {
+ CompletableFuture<Void> disconnectedToneFuture = new CompletableFuture<Void>()
+ .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
+ mDisconnectedToneBtFutures.get(call.getId()).thenRunAsync(() -> {
+ Log.i(this, "updateCall: Sending call disconnected update to BT ICS.");
+ updateCallToIcs(inCallService, info, parcelableCall, componentName);
+ mDisconnectedToneBtFutures.remove(call.getId());
+ }, new LoggedHandlerExecutor(mHandler, "ICC.uC", mLock));
+ } else {
+ updateCallToIcs(inCallService, info, parcelableCall, componentName);
}
}
Log.i(this, "Components updated: %s", componentsUpdated);
@@ -2666,12 +2741,27 @@
}
}
+ private void updateCallToIcs(IInCallService inCallService, InCallServiceInfo info,
+ ParcelableCall parcelableCall, ComponentName componentName) {
+ try {
+ inCallService.updateCall(
+ sanitizeParcelableCallForService(info, parcelableCall));
+ } catch (RemoteException exception) {
+ Log.w(this, "Call status update did not send to: "
+ + componentName + " successfully with error " + exception);
+ }
+ }
+
/**
* Adds the call to the list of calls tracked by the {@link InCallController}.
* @param call The call to add.
*/
@VisibleForTesting
public void addCall(Call call) {
+ if (call == null) {
+ return;
+ }
+
if (mCallIdMapper.getCalls().size() == 0) {
mAppOpsManager.startWatchingActive(new String[] { OPSTR_RECORD_AUDIO },
java.lang.Runnable::run, this);
@@ -2682,12 +2772,12 @@
if (mCallIdMapper.getCallId(call) == null) {
mCallIdMapper.addCall(call);
call.addListener(mCallListener);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ mPendingEndToneCall.add(call);
+ }
}
maybeTrackMicrophoneUse(isMuted());
- if (mFeatureFlags.separatelyBindToBtIncallService()) {
- mPendingEndToneCall.add(call);
- }
}
/**
@@ -2717,6 +2807,23 @@
}
/**
+ * @return A future that is pending whenever we are in the middle of binding to the BT
+ * incall service.
+ */
+ public CompletableFuture<Boolean> getBtBindingFuture(Call call) {
+ UserHandle userHandle = getUserFromCall(call);
+ return mBtBindingFuture.get(userHandle);
+ }
+
+ /**
+ * @return A future that is pending whenever we are in the process of sending the call
+ * disconnected state to the BT ICS so that the disconnect tone can finish playing.
+ */
+ public Map<String, CompletableFuture<Void>> getDisconnectedToneBtFutures() {
+ return mDisconnectedToneBtFutures;
+ }
+
+ /**
* Dumps the state of the {@link InCallController}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
@@ -3021,19 +3128,38 @@
mIsCallUsingMicrophone = mIsTrackingManagedAliveCall && !isMuted
&& !isCarrierPrivilegedUsingMicDuringVoipCall();
if (wasUsingMicrophone != mIsCallUsingMicrophone) {
+ int opPackageUid = getOpPackageUid();
if (mIsCallUsingMicrophone) {
// Note, not checking return value, as this op call is merely for tracing use
- mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+ mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, opPackageUid,
mContext.getOpPackageName(), false, null, null);
mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.MICROPHONE);
} else {
- mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+ mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, opPackageUid,
mContext.getOpPackageName(), null);
}
}
}
/**
+ * Returns the uid of the package in the current user to be used for app ops attribution.
+ */
+ private int getOpPackageUid() {
+ UserHandle user = mCallsManager.getCurrentUserHandle();
+
+ try {
+ PackageManager pkgManager = mContext.getPackageManager();
+ return pkgManager.getPackageUidAsUser(mContext.getOpPackageName(),
+ user.getIdentifier());
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(this, e, "getPackageForAssociatedUser: could not find package %s"
+ + " for user %s", mContext.getOpPackageName(), user);
+ // fallback to current process id - this should not happen
+ return myUid();
+ }
+ }
+
+ /**
* @return {@code true} if InCallController is tracking a managed call (i.e. not self managed
* and not external) that is active.
*/
@@ -3113,7 +3239,13 @@
return mCallsManager.getCurrentUserHandle();
} else {
UserHandle userFromCall = call.getAssociatedUser();
- UserManager userManager = mContext.getSystemService(UserManager.class);
+ UserManager userManager = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? mContext.createContextAsUser(mCallsManager.getCurrentUserHandle(), 0)
+ .getSystemService(UserManager.class)
+ : mContext.getSystemService(UserManager.class);
+ boolean isCurrentUserAdmin = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? userManager.isAdminUser()
+ : userManager.isUserAdmin(mCallsManager.getCurrentUserHandle().getIdentifier());
// 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
@@ -3123,8 +3255,7 @@
&& (userManager.isQuietModeEnabled(userFromCall)
// We should also account for secondary/guest users where the profile may not
// necessarily be paused.
- || !userManager.isUserAdmin(mCallsManager.getCurrentUserHandle()
- .getIdentifier()))) {
+ || !isCurrentUserAdmin)) {
return mCallsManager.getCurrentUserHandle();
}
return userFromCall;
@@ -3149,7 +3280,10 @@
}
}
}
- return false;
+ // If early binding for BT ICS is enabled, ensure that it is included into consideration as
+ // a bound non-UI ICS.
+ return mFeatureFlags.separatelyBindToBtIncallService() && !mBTInCallServices.isEmpty()
+ && isBluetoothPackage(packageName);
}
private void updateCombinedInCallServiceMap(UserHandle user) {
@@ -3183,4 +3317,13 @@
}
}
}
+
+ private boolean isBluetoothPackage(String packageName) {
+ for (String pkgName : mDefaultDialerCache.getBTInCallServicePackages()) {
+ if (pkgName.equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 0d6acd5..d98ebfe 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -139,8 +139,10 @@
public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
public static final String START_CONNECTION = "START_CONNECTION";
public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
+ public static final String CREATE_CONNECTION_TIMEOUT = "CREATE_CONNECTION_TIMEOUT";
public static final String START_CONFERENCE = "START_CONFERENCE";
public static final String CREATE_CONFERENCE_FAILED = "CREATE_CONFERENCE_FAILED";
+ public static final String CREATE_CONFERENCE_TIMEOUT = "CREATE_CONFERENCE_TIMEOUT";
public static final String BIND_CS = "BIND_CS";
public static final String CS_BOUND = "CS_BOUND";
public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 6070baa..c24ac97 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -623,6 +623,9 @@
try {
return mContext.getSystemService(TelephonyManager.class).isEmergencyNumber(
number);
+ } catch (UnsupportedOperationException uoe) {
+ Log.w(this, "isEmergencyNumber: Telephony not supported");
+ return false;
} catch (Exception e) {
Log.e(this, e, "isEmergencyNumber: Telephony threw an exception.");
return false;
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 8de62ed..f9cdc35 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -17,10 +17,17 @@
package com.android.server.telecom;
import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import android.bluetooth.BluetoothDevice;
import android.media.AudioManager;
+import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
-import java.util.ArrayList;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+import java.util.Set;
/**
* Used to represent the intermediate state during audio route switching.
@@ -32,6 +39,7 @@
public class PendingAudioRoute {
private CallAudioRouteController mCallAudioRouteController;
private AudioManager mAudioManager;
+ private BluetoothRouteManager mBluetoothRouteManager;
/**
* The {@link AudioRoute} that this pending audio switching started with
*/
@@ -41,17 +49,25 @@
* by new switching request during the ongoing switching
*/
private AudioRoute mDestRoute;
- private ArrayList<Integer> mPendingMessages;
+ private Set<Pair<Integer, String>> mPendingMessages;
private boolean mActive;
- PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager) {
+ /**
+ * The device that has been set for communication by Telecom
+ */
+ private @AudioRoute.AudioRouteType int mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+
+ PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager,
+ BluetoothRouteManager bluetoothRouteManager) {
mCallAudioRouteController = controller;
mAudioManager = audioManager;
- mPendingMessages = new ArrayList<>();
+ mBluetoothRouteManager = bluetoothRouteManager;
+ mPendingMessages = new ArraySet<>();
mActive = false;
+ mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
}
void setOrigRoute(boolean active, AudioRoute origRoute) {
- origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager);
+ origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
mOrigRoute = origRoute;
}
@@ -59,30 +75,33 @@
return mOrigRoute;
}
- void setDestRoute(boolean active, AudioRoute destRoute) {
- destRoute.onDestRouteAsPendingRoute(active, this, mAudioManager);
+ void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+ boolean isScoAudioConnected) {
+ destRoute.onDestRouteAsPendingRoute(active, this, device,
+ mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
mActive = active;
mDestRoute = destRoute;
}
- AudioRoute getDestRoute() {
+ public AudioRoute getDestRoute() {
return mDestRoute;
}
- public void addMessage(int message) {
- mPendingMessages.add(message);
+ public void addMessage(int message, String bluetoothDevice) {
+ mPendingMessages.add(new Pair<>(message, bluetoothDevice));
}
- public void onMessageReceived(int message) {
- if (message == PENDING_ROUTE_FAILED) {
+ public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
+ Log.i(this, "onMessageReceived: message - %s", message);
+ if (message.first == PENDING_ROUTE_FAILED) {
// Fallback to base route
- mDestRoute = mCallAudioRouteController.getBaseRoute(true);
mCallAudioRouteController.sendMessageWithSessionInfo(
- CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
+ return;
}
// Removes the first occurrence of the specified message from this list, if it is present.
- mPendingMessages.remove((Object) message);
+ mPendingMessages.remove(message);
evaluatePendingState();
}
@@ -90,10 +109,33 @@
if (mPendingMessages.isEmpty()) {
mCallAudioRouteController.sendMessageWithSessionInfo(
CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ } else {
+ Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
}
}
+ public void clearPendingMessages() {
+ mPendingMessages.clear();
+ }
+
+ public void clearPendingMessage(Pair<Integer, String> message) {
+ mPendingMessages.remove(message);
+ }
+
public boolean isActive() {
return mActive;
}
+
+ public @AudioRoute.AudioRouteType int getCommunicationDeviceType() {
+ return mCommunicationDeviceType;
+ }
+
+ public void setCommunicationDeviceType(
+ @AudioRoute.AudioRouteType int communicationDeviceType) {
+ mCommunicationDeviceType = communicationDeviceType;
+ }
+
+ public void overrideDestRoute(AudioRoute route) {
+ mDestRoute = route;
+ }
}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index fc90edd..9256387 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -185,30 +185,35 @@
private final PhoneAccountRegistrarWriteLock mWriteLock =
new PhoneAccountRegistrarWriteLock() {};
private final FeatureFlags mTelephonyFeatureFlags;
+ private final com.android.server.telecom.flags.FeatureFlags mTelecomFeatureFlags;
@VisibleForTesting
public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock,
DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
- FeatureFlags telephonyFeatureFlags) {
- this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy, telephonyFeatureFlags);
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) {
+ this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy,
+ telephonyFeatureFlags, telecomFeatureFlags);
}
@VisibleForTesting
public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock, String fileName,
DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
- FeatureFlags telephonyFeatureFlags) {
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) {
mAtomicFile = new AtomicFile(new File(context.getFilesDir(), fileName));
mState = new State();
mContext = context;
mLock = lock;
- mUserManager = UserManager.get(context);
+ mUserManager = context.getSystemService(UserManager.class);
mDefaultDialerCache = defaultDialerCache;
mSubscriptionManager = SubscriptionManager.from(mContext);
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAppLabelProxy = appLabelProxy;
mCurrentUserHandle = Process.myUserHandle();
+ mTelecomFeatureFlags = telecomFeatureFlags;
if (telephonyFeatureFlags != null) {
mTelephonyFeatureFlags = telephonyFeatureFlags;
@@ -237,7 +242,11 @@
PhoneAccount account = getPhoneAccountUnchecked(accountHandle);
if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
- return mTelephonyManager.getSubscriptionId(accountHandle);
+ try {
+ return mTelephonyManager.getSubscriptionId(accountHandle);
+ } catch (UnsupportedOperationException ignored) {
+ // Ignore; fall back to invalid below.
+ }
}
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
@@ -420,18 +429,23 @@
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");
+ try {
+ 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");
+ }
+ } catch (UnsupportedOperationException uoe) {
+ Log.w(this, "setUserSelectedOutgoingPhoneAccount: no telephony");
}
}
@@ -460,8 +474,13 @@
}
boolean isUserSelectedSmsPhoneAccount(PhoneAccountHandle accountHandle) {
- return getSubscriptionIdForPhoneAccount(accountHandle) ==
- SubscriptionManager.getDefaultSmsSubscriptionId();
+ try {
+ return getSubscriptionIdForPhoneAccount(accountHandle) ==
+ SubscriptionManager.getDefaultSmsSubscriptionId();
+ } catch (UnsupportedOperationException uoe) {
+ Log.w(this, "isUserSelectedSmsPhoneAccount: no telephony");
+ return false;
+ }
}
public ComponentName getSystemSimCallManagerComponent() {
@@ -470,12 +489,18 @@
public ComponentName getSystemSimCallManagerComponent(int subId) {
String defaultSimCallManager = null;
- CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
- Context.CARRIER_CONFIG_SERVICE);
- PersistableBundle configBundle = configManager.getConfigForSubId(subId);
- if (configBundle != null) {
- defaultSimCallManager = configBundle.getString(
- CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+ try {
+ CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
+ Context.CARRIER_CONFIG_SERVICE);
+ if (configManager == null) return null;
+ PersistableBundle configBundle = configManager.getConfigForSubId(subId);
+ if (configBundle != null) {
+ defaultSimCallManager = configBundle.getString(
+ CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+ }
+ } catch (UnsupportedOperationException ignored) {
+ Log.w(this, "getSystemSimCallManagerComponent: no telephony");
+ // Fall through to empty below.
}
return TextUtils.isEmpty(defaultSimCallManager)
? null : ComponentName.unflattenFromString(defaultSimCallManager);
@@ -757,8 +782,11 @@
}
if (acrossProfiles) {
- return UserManager.get(mContext).isSameProfileGroup(userHandle.getIdentifier(),
- phoneAccountUserHandle.getIdentifier());
+ UserManager um = mContext.getSystemService(UserManager.class);
+ return mTelecomFeatureFlags.telecomResolveHiddenDependencies()
+ ? um.isSameProfileGroup(userHandle, phoneAccountUserHandle)
+ : um.isSameProfileGroup(userHandle.getIdentifier(),
+ phoneAccountUserHandle.getIdentifier());
} else {
return phoneAccountUserHandle.equals(userHandle);
}
@@ -1456,16 +1484,22 @@
"Notifying telephony of voice service override change for %d SIMs, hasService = %b",
simHandlesToNotify.size(),
hasService);
- for (PhoneAccountHandle simHandle : simHandlesToNotify) {
- // This may be null if there are no active SIMs but the device is still camped for
- // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
- TelephonyManager simTm = mTelephonyManager.createForPhoneAccountHandle(simHandle);
- if (simTm == null) {
- Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
- + "simTm is null.");
- continue;
+ try {
+ for (PhoneAccountHandle simHandle : simHandlesToNotify) {
+ // This may be null if there are no active SIMs but the device is still camped for
+ // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
+ TelephonyManager simTm = mTelephonyManager.createForPhoneAccountHandle(simHandle);
+ if (simTm == null) {
+ Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
+ + "simTm is null.");
+ continue;
+ }
+ simTm.setVoiceServiceStateOverride(hasService);
}
- simTm.setVoiceServiceStateOverride(hasService);
+ } catch (UnsupportedOperationException ignored) {
+ // No telephony, so we can't override the sim service state.
+ // Realistically we shouldn't get here because there should be no sim subs in this case.
+ Log.w(this, "maybeNotifyTelephonyForVoiceServiceState: no telephony");
}
}
@@ -1832,7 +1866,12 @@
} else {
pw.println(defaultOutgoing);
}
- pw.println("defaultVoiceSubId: " + SubscriptionManager.getDefaultVoiceSubscriptionId());
+ // SubscriptionManager will throw if FEATURE_TELEPHONY_SUBSCRIPTION is not present.
+ if (mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+ pw.println("defaultVoiceSubId: "
+ + SubscriptionManager.getDefaultVoiceSubscriptionId());
+ }
pw.println("simCallManager: " + getSimCallManager(mCurrentUserHandle));
pw.println("phoneAccounts:");
pw.increaseIndent();
@@ -1942,7 +1981,7 @@
try {
XmlPullParser parser = Xml.resolvePullParser(is);
parser.nextTag();
- mState = readFromXml(parser, mContext, mTelephonyFeatureFlags);
+ mState = readFromXml(parser, mContext, mTelephonyFeatureFlags, mTelecomFeatureFlags);
migratePhoneAccountHandle(mState);
versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
@@ -1983,8 +2022,11 @@
}
private static State readFromXml(XmlPullParser parser, Context context,
- FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
- State s = sStateXml.readFromXml(parser, 0, context, telephonyFeatureFlags);
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+ throws IOException, XmlPullParserException {
+ State s = sStateXml.readFromXml(parser, 0, context,
+ telephonyFeatureFlags, telecomFeatureFlags);
return s != null ? s : new State();
}
@@ -2061,7 +2103,9 @@
* 'parser' if it does not recognize the data it sees.
*/
public abstract T readFromXml(XmlPullParser parser, int version, Context context,
- FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException;
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags featureFlags)
+ throws IOException, XmlPullParserException;
protected void writeTextIfNonNull(String tagName, Object value, XmlSerializer serializer)
throws IOException {
@@ -2190,7 +2234,8 @@
}
protected Set<PhoneAccountHandle> readPhoneAccountHandleSet(XmlPullParser parser,
- int version, Context context, FeatureFlags telephonyFeatureFlags)
+ int version, Context context, FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws IOException, XmlPullParserException {
int length = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_LENGTH));
Set<PhoneAccountHandle> handles = new HashSet<>(length);
@@ -2199,7 +2244,7 @@
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
handles.add(sPhoneAccountHandleXml.readFromXml(parser, version, context,
- telephonyFeatureFlags));
+ telephonyFeatureFlags, telecomFeatureFlags));
}
return handles;
}
@@ -2338,7 +2383,9 @@
@Override
public State readFromXml(XmlPullParser parser, int version, Context context,
- FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+ throws IOException, XmlPullParserException {
if (parser.getName().equals(CLASS_STATE)) {
State s = new State();
@@ -2355,16 +2402,23 @@
parser.nextTag();
PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
.readFromXml(parser, s.versionNumber, context,
- telephonyFeatureFlags);
- UserManager userManager = UserManager.get(context);
- UserInfo primaryUser = userManager.getPrimaryUser();
+ telephonyFeatureFlags, telecomFeatureFlags);
+ UserManager userManager = context.getSystemService(UserManager.class);
+ // UserManager#getMainUser requires either the MANAGE_USERS,
+ // CREATE_USERS, or QUERY_USERS permission.
+ UserHandle primaryUser = userManager.getMainUser();
+ UserInfo primaryUserInfo = userManager.getPrimaryUser();
+ if (!telecomFeatureFlags.telecomResolveHiddenDependencies()) {
+ primaryUser = primaryUserInfo != null
+ ? primaryUserInfo.getUserHandle()
+ : null;
+ }
if (primaryUser != null) {
- UserHandle userHandle = primaryUser.getUserHandle();
DefaultPhoneAccountHandle defaultPhoneAccountHandle
- = new DefaultPhoneAccountHandle(userHandle,
+ = new DefaultPhoneAccountHandle(primaryUser,
phoneAccountHandle, "" /* groupId */);
s.defaultOutgoingAccountHandles
- .put(userHandle, defaultPhoneAccountHandle);
+ .put(primaryUser, defaultPhoneAccountHandle);
}
} else {
int defaultAccountHandlesDepth = parser.getDepth();
@@ -2372,7 +2426,7 @@
DefaultPhoneAccountHandle accountHandle
= sDefaultPhoneAccountHandleXml
.readFromXml(parser, s.versionNumber, context,
- telephonyFeatureFlags);
+ telephonyFeatureFlags, telecomFeatureFlags);
if (accountHandle != null && s.accounts != null) {
s.defaultOutgoingAccountHandles
.put(accountHandle.userHandle, accountHandle);
@@ -2383,7 +2437,8 @@
int accountsDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
- s.versionNumber, context, telephonyFeatureFlags);
+ s.versionNumber, context, telephonyFeatureFlags,
+ telecomFeatureFlags);
if (account != null && s.accounts != null) {
s.accounts.add(account);
@@ -2410,7 +2465,7 @@
public void writeToXml(DefaultPhoneAccountHandle o, XmlSerializer serializer,
Context context, FeatureFlags telephonyFeatureFlags) throws IOException {
if (o != null) {
- final UserManager userManager = UserManager.get(context);
+ final UserManager userManager = context.getSystemService(UserManager.class);
final long serialNumber = userManager.getSerialNumberForUser(o.userHandle);
if (serialNumber != -1) {
serializer.startTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
@@ -2427,7 +2482,8 @@
@Override
public DefaultPhoneAccountHandle readFromXml(XmlPullParser parser, int version,
- Context context, FeatureFlags telephonyFeatureFlags)
+ Context context, FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws IOException, XmlPullParserException {
if (parser.getName().equals(CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE)) {
int outerDepth = parser.getDepth();
@@ -2438,7 +2494,7 @@
if (parser.getName().equals(ACCOUNT_HANDLE)) {
parser.nextTag();
accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
- context, telephonyFeatureFlags);
+ context, telephonyFeatureFlags, telecomFeatureFlags);
} else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
parser.next();
userSerialNumberString = parser.getText();
@@ -2452,7 +2508,7 @@
if (userSerialNumberString != null) {
try {
long serialNumber = Long.parseLong(userSerialNumberString);
- userHandle = UserManager.get(context)
+ userHandle = context.getSystemService(UserManager.class)
.getUserForSerialNumber(serialNumber);
} catch (NumberFormatException e) {
Log.e(this, e,
@@ -2530,7 +2586,8 @@
}
public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context,
- FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) throws IOException, XmlPullParserException {
if (parser.getName().equals(CLASS_PHONE_ACCOUNT)) {
int outerDepth = parser.getDepth();
PhoneAccountHandle accountHandle = null;
@@ -2555,7 +2612,7 @@
if (parser.getName().equals(ACCOUNT_HANDLE)) {
parser.nextTag();
accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
- context, telephonyFeatureFlags);
+ context, telephonyFeatureFlags, telecomFeatureFlags);
} else if (parser.getName().equals(ADDRESS)) {
parser.next();
address = Uri.parse(parser.getText());
@@ -2605,7 +2662,7 @@
// this info is in the XML for parsing reasons. We only flag setting the
// parsed value below based on the flag.
simultaneousCallingRestriction = readPhoneAccountHandleSet(parser, version,
- context, telephonyFeatureFlags);
+ context, telephonyFeatureFlags, telecomFeatureFlags);
}
}
@@ -2734,7 +2791,7 @@
writeTextIfNonNull(ID, o.getId(), serializer);
if (o.getUserHandle() != null && context != null) {
- UserManager userManager = UserManager.get(context);
+ UserManager userManager = context.getSystemService(UserManager.class);
writeLong(USER_SERIAL_NUMBER,
userManager.getSerialNumberForUser(o.getUserHandle()), serializer);
}
@@ -2745,14 +2802,16 @@
@Override
public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context,
- FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+ throws IOException, XmlPullParserException {
if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
String componentNameString = null;
String idString = null;
String userSerialNumberString = null;
int outerDepth = parser.getDepth();
- UserManager userManager = UserManager.get(context);
+ UserManager userManager = context.getSystemService(UserManager.class);
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (parser.getName().equals(COMPONENT_NAME)) {
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 490db85..11f5e02 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -16,6 +16,7 @@
package com.android.server.telecom;
+import android.content.pm.PackageManager;
import android.telecom.Log;
import android.telephony.PhoneNumberUtils;
import android.telephony.SubscriptionInfo;
@@ -24,6 +25,8 @@
import android.telephony.TelephonyRegistryManager;
import android.telephony.emergency.EmergencyNumber;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -32,7 +35,7 @@
* Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
* changes.
*/
-final class PhoneStateBroadcaster extends CallsManagerListenerBase {
+public final class PhoneStateBroadcaster extends CallsManagerListenerBase {
private final CallsManager mCallsManager;
private final TelephonyRegistryManager mRegistry;
@@ -136,25 +139,34 @@
.flatMap(List::stream)
.filter(numberObj -> Objects.equals(numberObj.getNumber(), strippedNumber))
.findFirst();
+ } catch (UnsupportedOperationException ignored) {
+ emergencyNumber = Optional.empty();
} catch (IllegalStateException ie) {
emergencyNumber = Optional.empty();
} catch (RuntimeException r) {
emergencyNumber = Optional.empty();
}
- int subscriptionId = tm.getSubscriptionId(call.getTargetPhoneAccount());
- SubscriptionManager subscriptionManager =
- mCallsManager.getContext().getSystemService(SubscriptionManager.class);
- int simSlotIndex = SubscriptionManager.DEFAULT_PHONE_INDEX;
- if (subscriptionManager != null) {
- SubscriptionInfo subInfo =
- subscriptionManager.getActiveSubscriptionInfo(subscriptionId);
- if (subInfo != null) {
- simSlotIndex = subInfo.getSimSlotIndex();
- }
- }
-
if (emergencyNumber.isPresent()) {
+ int subscriptionId;
+ int simSlotIndex;
+ try {
+ subscriptionId = tm.getSubscriptionId(call.getTargetPhoneAccount());
+ SubscriptionManager subscriptionManager =
+ mCallsManager.getContext().getSystemService(SubscriptionManager.class);
+ simSlotIndex = SubscriptionManager.DEFAULT_PHONE_INDEX;
+ if (subscriptionManager != null) {
+ SubscriptionInfo subInfo =
+ subscriptionManager.getActiveSubscriptionInfo(subscriptionId);
+ if (subInfo != null) {
+ simSlotIndex = subInfo.getSimSlotIndex();
+ }
+ }
+ } catch (UnsupportedOperationException ignored) {
+ subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ simSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+ }
+
mRegistry.notifyOutgoingEmergencyCall(
simSlotIndex, subscriptionId, emergencyNumber.get());
}
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 0e0b99f..16fa0c4 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -33,6 +33,8 @@
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
+
import android.telecom.CallerInfo;
import android.util.Pair;
@@ -48,10 +50,12 @@
private final Context mContext;
private final CallsManager mCallsManager;
+ private FeatureFlags mFeatureFlags;
- public RingtoneFactory(CallsManager callsManager, Context context) {
+ public RingtoneFactory(CallsManager callsManager, Context context, FeatureFlags featureFlags) {
mContext = context;
mCallsManager = callsManager;
+ mFeatureFlags = featureFlags;
}
public Pair<Uri, Ringtone> getRingtone(Call incomingCall,
@@ -82,8 +86,12 @@
// Contact didn't specify ringtone or custom Ringtone creation failed. Get default
// ringtone for user or profile.
Context contextToUse = hasDefaultRingtoneForUser(userContext) ? userContext : mContext;
+ UserManager um = contextToUse.getSystemService(UserManager.class);
+ boolean isUserUnlocked = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? um.isUserUnlocked(contextToUse.getUser())
+ : um.isUserUnlocked(contextToUse.getUserId());
Uri defaultRingtoneUri;
- if (UserManager.get(contextToUse).isUserUnlocked(contextToUse.getUserId())) {
+ if (isUserUnlocked) {
defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(contextToUse,
RingtoneManager.TYPE_RINGTONE);
if (defaultRingtoneUri == null) {
@@ -138,22 +146,38 @@
}
private Context getWorkProfileContextForUser(UserHandle userHandle) {
- // UserManager.getEnabledProfiles returns the enabled profiles along with the user's handle
- // itself (so we must filter out the user).
- List<UserInfo> profiles = UserManager.get(mContext).getEnabledProfiles(
- userHandle.getIdentifier());
- UserInfo workprofile = null;
+ // UserManager.getUserProfiles returns the enabled profiles along with the context user's
+ // handle itself (so we must filter out the user).
+ Context userContext = mContext.createContextAsUser(userHandle, 0);
+ UserManager um = mFeatureFlags.telecomResolveHiddenDependencies()
+ ? userContext.getSystemService(UserManager.class)
+ : mContext.getSystemService(UserManager.class);
+ List<UserHandle> profiles = um.getUserProfiles();
+ List<UserInfo> userInfoProfiles = um.getEnabledProfiles(userHandle.getIdentifier());
+ UserHandle workProfileUser = null;
int managedProfileCount = 0;
- for (UserInfo profile : profiles) {
- UserHandle profileUserHandle = profile.getUserHandle();
- if (profileUserHandle != userHandle && profile.isManagedProfile()) {
- managedProfileCount++;
- workprofile = profile;
+
+ if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+ for (UserHandle profileUser : profiles) {
+ UserManager userManager = mContext.createContextAsUser(profileUser, 0)
+ .getSystemService(UserManager.class);
+ if (!userHandle.equals(profileUser) && userManager.isManagedProfile()) {
+ managedProfileCount++;
+ workProfileUser = profileUser;
+ }
+ }
+ } else {
+ for(UserInfo profile: userInfoProfiles) {
+ UserHandle profileUserHandle = profile.getUserHandle();
+ if (!profileUserHandle.equals(userHandle) && profile.isManagedProfile()) {
+ managedProfileCount++;
+ workProfileUser = profileUserHandle;
+ }
}
}
// There may be many different types of profiles, so only count Managed (Work) Profiles.
if(managedProfileCount == 1) {
- return getContextForUserHandle(workprofile.getUserHandle());
+ return getContextForUserHandle(workProfileUser);
}
// There are multiple managed profiles for the associated user and we do not have enough
// info to determine which profile is the work profile. Just use the default.
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index 9f515e6..1b5c71b 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -71,7 +71,7 @@
* bt in-call service role.
* @return the package name of the package filling the role, {@code null} otherwise.
*/
- String getBTInCallService();
+ String[] getBTInCallService();
/**
* Override the {@link android.app.role.RoleManager} bt in-call service package with another
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index 33ec466..ded4d9c 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -78,9 +78,9 @@
}
@Override
- public String getBTInCallService() {
+ public String[] getBTInCallService() {
if (mOverrideBTInCallService != null) {
- return mOverrideBTInCallService;
+ return new String [] {mOverrideBTInCallService};
}
return getBluetoothInCallServicePackageName();
}
@@ -166,8 +166,8 @@
return roleHolders.get(0);
}
- private String getBluetoothInCallServicePackageName() {
- return mContext.getResources().getString(R.string.system_bluetooth_stack);
+ private String[] getBluetoothInCallServicePackageName() {
+ return mContext.getResources().getStringArray(R.array.system_bluetooth_stack_package_name);
}
/**
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 77f7b2e..a18042b 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -241,7 +241,7 @@
* Abbreviated form of the package name from {@link #mComponentName}; used for session logging.
*/
protected final String mPackageAbbreviation;
- private final FeatureFlags mFlags;
+ protected final FeatureFlags mFlags;
/** The set of callbacks waiting for notification of the binding's success or failure. */
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 9314d7e..20320f2 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -53,13 +53,18 @@
import android.os.Build;
import android.os.Bundle;
import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
import android.os.UserHandle;
import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import android.provider.Settings;
import android.telecom.CallAttributes;
import android.telecom.CallException;
+import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -72,12 +77,14 @@
import android.util.EventLog;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.BasicShellCommandHandler;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.settings.BlockedNumbersActivity;
@@ -90,8 +97,10 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Method;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -206,11 +215,11 @@
switch (callAttributes.getDirection()) {
case DIRECTION_OUTGOING:
transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
- mCallsManager, extras);
+ mCallsManager, extras, mFeatureFlags);
break;
case DIRECTION_INCOMING:
transaction = new IncomingCallTransaction(callId, callAttributes,
- mCallsManager, extras);
+ mCallsManager, extras, mFeatureFlags);
break;
default:
throw new IllegalArgumentException(String.format("Invalid Call Direction. "
@@ -238,6 +247,7 @@
callEventCallback, mCallsManager, call);
call.setTransactionServiceWrapper(serviceWrapper);
+
if (mFeatureFlags.transactionalVideoState()) {
call.setTransactionalCallSupportsVideoCalling(callAttributes);
}
@@ -988,6 +998,9 @@
}
}
return getTelephonyManager(subId).getVoiceMailNumber();
+ } catch (UnsupportedOperationException ignored) {
+ Log.w(this, "getVoiceMailNumber: no Telephony");
+ return null;
} catch (Exception e) {
Log.e(this, e, "getSubscriptionIdForPhoneAccount");
throw e;
@@ -1024,6 +1037,9 @@
accountHandle);
}
return getTelephonyManager(subId).getLine1Number();
+ } catch (UnsupportedOperationException ignored) {
+ Log.w(this, "getLine1Number: no telephony");
+ return null;
} catch (Exception e) {
Log.e(this, e, "getSubscriptionIdForPhoneAccount");
throw e;
@@ -1502,8 +1518,13 @@
subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
accountHandle);
}
- retval = getTelephonyManager(subId)
- .handlePinMmiForSubscriber(subId, dialString);
+ try {
+ retval = getTelephonyManager(subId)
+ .handlePinMmiForSubscriber(subId, dialString);
+ } catch (UnsupportedOperationException uoe) {
+ Log.w(this, "handlePinMmiForPhoneAccount: no telephony");
+ retval = false;
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1651,7 +1672,13 @@
&& accountExtra != null && accountExtra.getBoolean(
PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
false)) {
- mCallsManager.getInCallController().bindToServices(null, false);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ mCallsManager.getInCallController().bindToBTService(
+ null, null);
+ }
+ // Should be able to run this as is even if above flag is
+ // enabled (BT binding should be skipped automatically).
+ mCallsManager.getInCallController().bindToServices(null);
}
}
} finally {
@@ -2004,7 +2031,11 @@
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
try {
- BlockedNumberContract.BlockedNumbers.endBlockSuppression(mContext);
+ if (mBlockedNumbersManager != null) {
+ mBlockedNumbersManager.endBlockSuppression();
+ } else {
+ BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2093,6 +2124,14 @@
}
}
+ @Override
+ public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ return new TelecomShellCommand(this, mContext).exec(this,
+ in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args);
+ }
+
/**
* Print all feature flag configurations that Telecom is using for debugging purposes.
*/
@@ -2102,12 +2141,24 @@
// 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();
+ int maxLength = Arrays.stream(methods)
+ .map(Method::getName)
+ .map(String::length)
+ .max(Integer::compare)
+ .get();
+ String format = "\t%s: %-" + maxLength + "s %s";
+
if (methods.length == 0) {
pw.println("NONE");
return;
}
+
for (Method m : methods) {
- pw.println(m.getName() + "-> " + m.invoke(mFeatureFlags));
+ String flagEnabled = (Boolean) m.invoke(mFeatureFlags) ? "[✅]": "[❌]";
+ String methodName = m.getName();
+ String camelCaseName = methodName.replaceAll("([a-z])([A-Z]+)", "$1_$2")
+ .toLowerCase(Locale.US);
+ pw.println(String.format(format, flagEnabled, methodName, camelCaseName));
}
} catch (Exception e) {
pw.println("[ERROR]");
@@ -2289,14 +2340,10 @@
}
/**
- * A method intended for use in testing to clean up any calls that get stuck in the
- * {@link CallState#DISCONNECTED} or {@link CallState#DISCONNECTING} states. Stuck
- * calls
- * during CTS cause cascading failures, so if the CTS test detects such a state, it
- * should
- * call this method via a shell command to clean up before moving on to the next
- * test.
- * Also cleans up any pending futures related to
+ * A method intended for use in testing to clean up any calls are ongoing. Stuck
+ * calls during CTS cause cascading failures, so if the CTS test detects such a state, it
+ * should call this method via a shell command to clean up before moving on to the next
+ * test. Also cleans up any pending futures related to
* {@link android.telecom.CallDiagnosticService}s.
*/
@Override
@@ -2309,11 +2356,19 @@
try {
Set<UserHandle> userHandles = new HashSet<>();
for (Call call : mCallsManager.getCalls()) {
- call.cleanup();
- if (call.getState() == CallState.DISCONNECTED
- || call.getState() == CallState.DISCONNECTING) {
- mCallsManager.markCallAsRemoved(call);
+ // Any call that is not in a disconnect* state should be moved to the
+ // disconnected state
+ if (!isDisconnectingOrDisconnected(call)) {
+ mCallsManager.markCallAsDisconnected(
+ call,
+ new DisconnectCause(DisconnectCause.OTHER,
+ "cleaning up stuck calls"));
}
+ // ensure the call is immediately removed from CallsManager instead of
+ // using a Future to do the work.
+ call.cleanup();
+ // finally, officially remove the call from CallsManager tracking
+ mCallsManager.markCallAsRemoved(call);
userHandles.add(call.getAssociatedUser());
}
for (UserHandle userHandle : userHandles) {
@@ -2328,6 +2383,11 @@
}
}
+ private boolean isDisconnectingOrDisconnected(Call call){
+ return call.getState() == CallState.DISCONNECTED
+ || call.getState() == CallState.DISCONNECTING;
+ }
+
/**
* A method intended for test to clean up orphan {@link PhoneAccount}. An orphan
* {@link PhoneAccount} is a phone account belongs to an invalid {@link UserHandle}
@@ -2564,34 +2624,26 @@
* @param packageName the package name of the app to check calls for.
* @param userHandle the user handle on which to check for calls.
* @param callingPackage The caller's package name.
- * @param detectForAllUsers indicates if calls should be detected across all users. If the
- * caller does not have the ability to interact across users, get
- * managed calls for the caller instead.
* @return {@code true} if there are ongoing calls, {@code false} otherwise.
*/
@Override
public boolean isInSelfManagedCall(String packageName, UserHandle userHandle,
- String callingPackage, boolean detectForAllUsers) {
+ String callingPackage) {
try {
mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
"READ_PRIVILEGED_PHONE_STATE required.");
// Ensure that the caller has the INTERACT_ACROSS_USERS permission if it's trying
// to access calls that don't belong to it.
- if (detectForAllUsers || (userHandle != null
- && !Binder.getCallingUserHandle().equals(userHandle))) {
+ if (!Binder.getCallingUserHandle().equals(userHandle)) {
enforceInAppCrossUserPermission();
- } else {
- // If INTERACT_ACROSS_USERS doesn't need to be enforced, ensure that the user
- // being checked is the caller.
- userHandle = Binder.getCallingUserHandle();
}
Log.startSession("TSI.iISMC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
try {
- return mCallsManager.isInSelfManagedCallCrossUsers(
- packageName, userHandle, detectForAllUsers);
+ return mCallsManager.isInSelfManagedCall(
+ packageName, userHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2668,10 +2720,10 @@
private final TelecomSystem.SyncRoot mLock;
private TransactionManager mTransactionManager;
private final TransactionalServiceRepository mTransactionalServiceRepository;
+ private final BlockedNumbersManager mBlockedNumbersManager;
private final FeatureFlags mFeatureFlags;
private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
-
public TelecomServiceImpl(
Context context,
CallsManager callsManager,
@@ -2719,6 +2771,9 @@
mTransactionManager = TransactionManager.getInstance();
mTransactionalServiceRepository = new TransactionalServiceRepository();
+ mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+ ? mContext.getSystemService(BlockedNumbersManager.class)
+ : null;
}
@VisibleForTesting
diff --git a/src/com/android/server/telecom/TelecomShellCommand.java b/src/com/android/server/telecom/TelecomShellCommand.java
new file mode 100644
index 0000000..557002c
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomShellCommand.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.sysprop.TelephonyProperties;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telecom.ITelecomService;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * Implements shell commands sent to telecom using the "adb shell cmd telecom..." command from shell
+ * or CTS.
+ */
+public class TelecomShellCommand extends BasicShellCommandHandler {
+ private static final String CALLING_PACKAGE = TelecomShellCommand.class.getPackageName();
+ private static final String COMMAND_SET_PHONE_ACCOUNT_ENABLED = "set-phone-account-enabled";
+ private static final String COMMAND_SET_PHONE_ACCOUNT_DISABLED = "set-phone-account-disabled";
+ private static final String COMMAND_REGISTER_PHONE_ACCOUNT = "register-phone-account";
+ private static final String COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT =
+ "set-user-selected-outgoing-phone-account";
+ private static final String COMMAND_REGISTER_SIM_PHONE_ACCOUNT = "register-sim-phone-account";
+ private static final String COMMAND_SET_TEST_CALL_REDIRECTION_APP =
+ "set-test-call-redirection-app";
+ private static final String COMMAND_SET_TEST_CALL_SCREENING_APP = "set-test-call-screening-app";
+ private static final String COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP =
+ "add-or-remove-call-companion-app";
+ private static final String COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT =
+ "set-phone-acct-suggestion-component";
+ private static final String COMMAND_UNREGISTER_PHONE_ACCOUNT = "unregister-phone-account";
+ private static final String COMMAND_SET_CALL_DIAGNOSTIC_SERVICE = "set-call-diagnostic-service";
+ private static final String COMMAND_SET_DEFAULT_DIALER = "set-default-dialer";
+ private static final String COMMAND_GET_DEFAULT_DIALER = "get-default-dialer";
+ private static final String COMMAND_STOP_BLOCK_SUPPRESSION = "stop-block-suppression";
+ private static final String COMMAND_CLEANUP_STUCK_CALLS = "cleanup-stuck-calls";
+ private static final String COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS =
+ "cleanup-orphan-phone-accounts";
+ private static final String COMMAND_RESET_CAR_MODE = "reset-car-mode";
+ private static final String COMMAND_IS_NON_IN_CALL_SERVICE_BOUND =
+ "is-non-ui-in-call-service-bound";
+
+ /**
+ * Change the system dialer package name if a package name was specified,
+ * Example: adb shell telecom set-system-dialer <PACKAGE>
+ *
+ * Restore it to the default if if argument is "default" or no argument is passed.
+ * Example: adb shell telecom set-system-dialer default
+ */
+ private static final String COMMAND_SET_SYSTEM_DIALER = "set-system-dialer";
+ private static final String COMMAND_GET_SYSTEM_DIALER = "get-system-dialer";
+ private static final String COMMAND_WAIT_ON_HANDLERS = "wait-on-handlers";
+ private static final String COMMAND_SET_SIM_COUNT = "set-sim-count";
+ private static final String COMMAND_GET_SIM_CONFIG = "get-sim-config";
+ private static final String COMMAND_GET_MAX_PHONES = "get-max-phones";
+ private static final String COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER =
+ "set-test-emergency-phone-account-package-filter";
+ /**
+ * Command used to emit a distinct "mark" in the logs.
+ */
+ private static final String COMMAND_LOG_MARK = "log-mark";
+
+ private final Context mContext;
+ private final ITelecomService mTelecomService;
+ private TelephonyManager mTelephonyManager;
+ private UserManager mUserManager;
+
+ public TelecomShellCommand(ITelecomService binder, Context context) {
+ mTelecomService = binder;
+ mContext = context;
+ }
+
+ @Override
+ public int onCommand(String command) {
+ if (command == null || command.isEmpty()) {
+ onHelp();
+ return 0;
+ }
+ try {
+ switch (command) {
+ case COMMAND_SET_PHONE_ACCOUNT_ENABLED:
+ runSetPhoneAccountEnabled(true);
+ break;
+ case COMMAND_SET_PHONE_ACCOUNT_DISABLED:
+ runSetPhoneAccountEnabled(false);
+ break;
+ case COMMAND_REGISTER_PHONE_ACCOUNT:
+ runRegisterPhoneAccount();
+ break;
+ case COMMAND_SET_TEST_CALL_REDIRECTION_APP:
+ runSetTestCallRedirectionApp();
+ break;
+ case COMMAND_SET_TEST_CALL_SCREENING_APP:
+ runSetTestCallScreeningApp();
+ break;
+ case COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP:
+ runAddOrRemoveCallCompanionApp();
+ break;
+ case COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT:
+ runSetTestPhoneAcctSuggestionComponent();
+ break;
+ case COMMAND_SET_CALL_DIAGNOSTIC_SERVICE:
+ runSetCallDiagnosticService();
+ break;
+ case COMMAND_REGISTER_SIM_PHONE_ACCOUNT:
+ runRegisterSimPhoneAccount();
+ break;
+ case COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT:
+ runSetUserSelectedOutgoingPhoneAccount();
+ break;
+ case COMMAND_UNREGISTER_PHONE_ACCOUNT:
+ runUnregisterPhoneAccount();
+ break;
+ case COMMAND_STOP_BLOCK_SUPPRESSION:
+ runStopBlockSuppression();
+ break;
+ case COMMAND_CLEANUP_STUCK_CALLS:
+ runCleanupStuckCalls();
+ break;
+ case COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS:
+ runCleanupOrphanPhoneAccounts();
+ break;
+ case COMMAND_RESET_CAR_MODE:
+ runResetCarMode();
+ break;
+ case COMMAND_SET_DEFAULT_DIALER:
+ runSetDefaultDialer();
+ break;
+ case COMMAND_GET_DEFAULT_DIALER:
+ runGetDefaultDialer();
+ break;
+ case COMMAND_SET_SYSTEM_DIALER:
+ runSetSystemDialer();
+ break;
+ case COMMAND_GET_SYSTEM_DIALER:
+ runGetSystemDialer();
+ break;
+ case COMMAND_WAIT_ON_HANDLERS:
+ runWaitOnHandler();
+ break;
+ case COMMAND_SET_SIM_COUNT:
+ runSetSimCount();
+ break;
+ case COMMAND_GET_SIM_CONFIG:
+ runGetSimConfig();
+ break;
+ case COMMAND_GET_MAX_PHONES:
+ runGetMaxPhones();
+ break;
+ case COMMAND_IS_NON_IN_CALL_SERVICE_BOUND:
+ runIsNonUiInCallServiceBound();
+ break;
+ case COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER:
+ runSetEmergencyPhoneAccountPackageFilter();
+ break;
+ case COMMAND_LOG_MARK:
+ runLogMark();
+ break;
+ default:
+ return handleDefaultCommands(command);
+ }
+ } catch (Exception e) {
+ getErrPrintWriter().println("Command["+ command + "]: Error: " + e);
+ return -1;
+ }
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ getOutPrintWriter().println("usage: telecom [subcommand] [options]\n"
+ + "usage: telecom set-phone-account-enabled <COMPONENT> <ID> <USER_SN>\n"
+ + "usage: telecom set-phone-account-disabled <COMPONENT> <ID> <USER_SN>\n"
+ + "usage: telecom register-phone-account <COMPONENT> <ID> <USER_SN> <LABEL>\n"
+ + "usage: telecom register-sim-phone-account [-e] <COMPONENT> <ID> <USER_SN>"
+ + " <LABEL>: registers a PhoneAccount with CAPABILITY_SIM_SUBSCRIPTION"
+ + " and optionally CAPABILITY_PLACE_EMERGENCY_CALLS if \"-e\" is provided\n"
+ + "usage: telecom set-user-selected-outgoing-phone-account [-e] <COMPONENT> <ID> "
+ + "<USER_SN>\n"
+ + "usage: telecom set-test-call-redirection-app <PACKAGE>\n"
+ + "usage: telecom set-test-call-screening-app <PACKAGE>\n"
+ + "usage: telecom set-phone-acct-suggestion-component <COMPONENT>\n"
+ + "usage: telecom add-or-remove-call-companion-app <PACKAGE> <1/0>\n"
+ + "usage: telecom register-sim-phone-account <COMPONENT> <ID> <USER_SN>"
+ + " <LABEL> <ADDRESS>\n"
+ + "usage: telecom unregister-phone-account <COMPONENT> <ID> <USER_SN>\n"
+ + "usage: telecom set-call-diagnostic-service <PACKAGE>\n"
+ + "usage: telecom set-default-dialer <PACKAGE>\n"
+ + "usage: telecom get-default-dialer\n"
+ + "usage: telecom get-system-dialer\n"
+ + "usage: telecom wait-on-handlers\n"
+ + "usage: telecom set-sim-count <COUNT>\n"
+ + "usage: telecom get-sim-config\n"
+ + "usage: telecom get-max-phones\n"
+ + "usage: telecom stop-block-suppression: Stop suppressing the blocked number"
+ + " provider after a call to emergency services.\n"
+ + "usage: telecom cleanup-stuck-calls: Clear any disconnected calls that have"
+ + " gotten wedged in Telecom.\n"
+ + "usage: telecom cleanup-orphan-phone-accounts: remove any phone accounts that"
+ + " no longer have a valid UserHandle or accounts that no longer belongs to an"
+ + " installed package.\n"
+ + "usage: telecom set-emer-phone-account-filter <PACKAGE>\n"
+ + "\n"
+ + "telecom set-phone-account-enabled: Enables the given phone account, if it has"
+ + " already been registered with Telecom.\n"
+ + "\n"
+ + "telecom set-phone-account-disabled: Disables the given phone account, if it"
+ + " has already been registered with telecom.\n"
+ + "\n"
+ + "telecom set-call-diagnostic-service: overrides call diagnostic service.\n"
+ + "telecom set-default-dialer: Sets the override default dialer to the given"
+ + " component; this will override whatever the dialer role is set to.\n"
+ + "\n"
+ + "telecom get-default-dialer: Displays the current default dialer.\n"
+ + "\n"
+ + "telecom get-system-dialer: Displays the current system dialer.\n"
+ + "telecom set-system-dialer: Set the override system dialer to the given"
+ + " component. To remove the override, send \"default\"\n"
+ + "\n"
+ + "telecom wait-on-handlers: Wait until all handlers finish their work.\n"
+ + "\n"
+ + "telecom set-sim-count: Set num SIMs (2 for DSDS, 1 for single SIM."
+ + " This may restart the device.\n"
+ + "\n"
+ + "telecom get-sim-config: Get the mSIM config string. \"DSDS\" for DSDS mode,"
+ + " or \"\" for single SIM\n"
+ + "\n"
+ + "telecom get-max-phones: Get the max supported phones from the modem.\n"
+ + "telecom set-test-emergency-phone-account-package-filter <PACKAGE>: sets a"
+ + " package name that will be used for test emergency calls. To clear,"
+ + " send an empty package name. Real emergency calls will still be placed"
+ + " over Telephony.\n"
+ + "telecom log-mark <MESSAGE>: emits a message into the telecom logs. Useful for "
+ + "testers to indicate where in the logs various test steps take place.\n"
+ + "telecom is-non-ui-in-call-service-bound <PACKAGE>: queries a particular "
+ + "non-ui-InCallService in InCallController to determine if it is bound \n"
+ );
+ }
+ private void runSetPhoneAccountEnabled(boolean enabled) throws RemoteException {
+ final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+ final boolean success = mTelecomService.enablePhoneAccount(handle, enabled);
+ if (success) {
+ getOutPrintWriter().println("Success - " + handle
+ + (enabled ? " enabled." : " disabled."));
+ } else {
+ getOutPrintWriter().println("Error - is " + handle + " a valid PhoneAccount?");
+ }
+ }
+
+ private void runRegisterPhoneAccount() throws RemoteException {
+ final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+ final String label = getNextArgRequired();
+ PhoneAccount account = PhoneAccount.builder(handle, label)
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+ mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
+ getOutPrintWriter().println("Success - " + handle + " registered.");
+ }
+
+ private void runRegisterSimPhoneAccount() throws RemoteException {
+ boolean isEmergencyAccount = false;
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-e": {
+ isEmergencyAccount = true;
+ break;
+ }
+ }
+ }
+ final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+ final String label = getNextArgRequired();
+ final String address = getNextArgRequired();
+ int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | (isEmergencyAccount ? PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS : 0);
+ PhoneAccount account = PhoneAccount.builder(
+ handle, label)
+ .setAddress(Uri.parse(address))
+ .setSubscriptionAddress(Uri.parse(address))
+ .setCapabilities(capabilities)
+ .setShortDescription(label)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+ .build();
+ mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
+ getOutPrintWriter().println("Success - " + handle + " registered.");
+ }
+
+ private void runSetTestCallRedirectionApp() throws RemoteException {
+ final String packageName = getNextArg();
+ mTelecomService.setTestDefaultCallRedirectionApp(packageName);
+ }
+
+ private void runSetTestCallScreeningApp() throws RemoteException {
+ final String packageName = getNextArg();
+ mTelecomService.setTestDefaultCallScreeningApp(packageName);
+ }
+
+ private void runAddOrRemoveCallCompanionApp() throws RemoteException {
+ final String packageName = getNextArgRequired();
+ String isAdded = getNextArgRequired();
+ boolean isAddedBool = "1".equals(isAdded);
+ mTelecomService.addOrRemoveTestCallCompanionApp(packageName, isAddedBool);
+ }
+
+ private void runSetCallDiagnosticService() throws RemoteException {
+ String packageName = getNextArg();
+ if ("default".equals(packageName)) packageName = null;
+ mTelecomService.setTestCallDiagnosticService(packageName);
+ getOutPrintWriter().println("Success - " + packageName
+ + " set as call diagnostic service.");
+ }
+
+ private void runSetTestPhoneAcctSuggestionComponent() throws RemoteException {
+ final String componentName = getNextArg();
+ mTelecomService.setTestPhoneAcctSuggestionComponent(componentName);
+ }
+
+ private void runSetUserSelectedOutgoingPhoneAccount() throws RemoteException {
+ Log.i(this, "runSetUserSelectedOutgoingPhoneAccount");
+ final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+ mTelecomService.setUserSelectedOutgoingPhoneAccount(handle);
+ getOutPrintWriter().println("Success - " + handle + " set as default outgoing account.");
+ }
+
+ private void runUnregisterPhoneAccount() throws RemoteException {
+ final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+ mTelecomService.unregisterPhoneAccount(handle, CALLING_PACKAGE);
+ getOutPrintWriter().println("Success - " + handle + " unregistered.");
+ }
+
+ private void runStopBlockSuppression() throws RemoteException {
+ mTelecomService.stopBlockSuppression();
+ }
+
+ private void runCleanupStuckCalls() throws RemoteException {
+ mTelecomService.cleanupStuckCalls();
+ }
+
+ private void runCleanupOrphanPhoneAccounts() throws RemoteException {
+ getOutPrintWriter().println("Success - cleaned up "
+ + mTelecomService.cleanupOrphanPhoneAccounts()
+ + " phone accounts.");
+ }
+
+ private void runResetCarMode() throws RemoteException {
+ mTelecomService.resetCarMode();
+ }
+
+ private void runSetDefaultDialer() throws RemoteException {
+ String packageName = getNextArg();
+ if ("default".equals(packageName)) packageName = null;
+ mTelecomService.setTestDefaultDialer(packageName);
+ getOutPrintWriter().println("Success - " + packageName
+ + " set as override default dialer.");
+ }
+
+ private void runSetSystemDialer() throws RemoteException {
+ final String flatComponentName = getNextArg();
+ final ComponentName componentName = (flatComponentName.equals("default")
+ ? null : parseComponentName(flatComponentName));
+ mTelecomService.setSystemDialer(componentName);
+ getOutPrintWriter().println("Success - " + componentName + " set as override system dialer.");
+ }
+
+ private void runGetDefaultDialer() throws RemoteException {
+ getOutPrintWriter().println(mTelecomService.getDefaultDialerPackage(CALLING_PACKAGE));
+ }
+
+ private void runGetSystemDialer() throws RemoteException {
+ getOutPrintWriter().println(mTelecomService.getSystemDialerPackage(CALLING_PACKAGE));
+ }
+
+ private void runWaitOnHandler() throws RemoteException {
+
+ }
+
+ private void runSetSimCount() throws RemoteException {
+ if (!callerIsRoot()) {
+ getOutPrintWriter().println("set-sim-count requires adb root");
+ return;
+ }
+ int numSims = Integer.parseInt(getNextArgRequired());
+ getOutPrintWriter().println("Setting sim count to " + numSims + ". Device may reboot");
+ getTelephonyManager().switchMultiSimConfig(numSims);
+ }
+
+ /**
+ * prints out whether a particular non-ui InCallServices is bound in a call
+ */
+ public void runIsNonUiInCallServiceBound() throws RemoteException {
+ if (TextUtils.isEmpty(peekNextArg())) {
+ getOutPrintWriter().println("No Argument passed. Please pass a <PACKAGE_NAME> to "
+ + "lookup.");
+ } else {
+ getOutPrintWriter().println(
+ String.valueOf(mTelecomService.isNonUiInCallServiceBound(getNextArg())));
+ }
+ }
+
+ /**
+ * Prints the mSIM config to the console.
+ * "DSDS" for a phone in DSDS mode
+ * "" (empty string) for a phone in SS mode
+ */
+ private void runGetSimConfig() throws RemoteException {
+ getOutPrintWriter().println(TelephonyProperties.multi_sim_config().orElse(""));
+ }
+
+ private void runGetMaxPhones() throws RemoteException {
+ // how many logical modems can be potentially active simultaneously
+ getOutPrintWriter().println(getTelephonyManager().getSupportedModemCount());
+ }
+
+ private void runSetEmergencyPhoneAccountPackageFilter() throws RemoteException {
+ String packageName = getNextArg();
+ if (TextUtils.isEmpty(packageName)) {
+ mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(null);
+ getOutPrintWriter().println("Success - filter cleared");
+ } else {
+ mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(packageName);
+ getOutPrintWriter().println("Success = filter set to " + packageName);
+ }
+
+ }
+
+ private void runLogMark() throws RemoteException {
+ String message = Arrays.stream(peekRemainingArgs()).collect(Collectors.joining(" "));
+ mTelecomService.requestLogMark(message);
+ }
+
+ private PhoneAccountHandle getPhoneAccountHandleFromArgs() throws RemoteException {
+ if (TextUtils.isEmpty(peekNextArg())) {
+ return null;
+ }
+ final ComponentName component = parseComponentName(getNextArgRequired());
+ final String accountId = getNextArgRequired();
+ final String userSnInStr = getNextArgRequired();
+ UserHandle userHandle;
+ try {
+ final int userSn = Integer.parseInt(userSnInStr);
+ userHandle = UserHandle.of(getUserManager().getUserHandle(userSn));
+ } catch (NumberFormatException ex) {
+ Log.w(this, "getPhoneAccountHandleFromArgs - invalid user %s", userSnInStr);
+ throw new IllegalArgumentException ("Invalid user serial number " + userSnInStr);
+ }
+ return new PhoneAccountHandle(component, accountId, userHandle);
+ }
+
+ private boolean callerIsRoot() {
+ return Process.ROOT_UID == Process.myUid();
+ }
+
+ private ComponentName parseComponentName(String component) {
+ ComponentName cn = ComponentName.unflattenFromString(component);
+ if (cn == null) {
+ throw new IllegalArgumentException ("Invalid component " + component);
+ }
+ return cn;
+ }
+
+ private TelephonyManager getTelephonyManager() throws IllegalStateException {
+ if (mTelephonyManager == null) {
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+ }
+ if (mTelephonyManager == null) {
+ Log.w(this, "getTelephonyManager: Can't access telephony service.");
+ throw new IllegalStateException("Could not access the Telephony Service. Is the system "
+ + "running?");
+ }
+ return mTelephonyManager;
+ }
+
+ private UserManager getUserManager() throws IllegalStateException {
+ if (mUserManager == null) {
+ mUserManager = mContext.getSystemService(UserManager.class);
+ }
+ if (mUserManager == null) {
+ Log.w(this, "getUserManager: Can't access UserManager service.");
+ throw new IllegalStateException("Could not access the UserManager Service. Is the "
+ + "system running?");
+ }
+ return mUserManager;
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 1d0300d..d7dcf38 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -29,7 +29,6 @@
import android.os.BugreportManager;
import android.os.DropBoxManager;
import android.os.UserHandle;
-import android.os.UserManager;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import android.telephony.AnomalyReporter;
@@ -45,9 +44,7 @@
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;
@@ -248,7 +245,7 @@
try {
mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName), null);
+ mContext.getPackageManager(), packageName), null, mFeatureFlags);
mContactsAsyncHelper = contactsAsyncHelperFactory.create(
new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -307,7 +304,7 @@
@Override
public CallEndpointController create(Context context, SyncRoot lock,
CallsManager callsManager) {
- return new CallEndpointController(context, callsManager);
+ return new CallEndpointController(context, callsManager, featureFlags);
}
};
@@ -349,22 +346,23 @@
ToastFactory toastFactory = new ToastFactory() {
@Override
- public Toast makeText(Context context, int resId, int duration) {
+ public void makeText(Context context, int resId, int duration) {
if (mFeatureFlags.telecomResolveHiddenDependencies()) {
- return Toast.makeText(context, resId, duration);
+ context.getMainExecutor().execute(() ->
+ Toast.makeText(context, resId, duration).show());
} else {
- return Toast.makeText(context, context.getMainLooper(),
- context.getString(resId),
- duration);
+ Toast.makeText(context, context.getMainLooper(),
+ context.getString(resId), duration).show();
}
}
@Override
- public Toast makeText(Context context, CharSequence text, int duration) {
+ public void makeText(Context context, CharSequence text, int duration) {
if (mFeatureFlags.telecomResolveHiddenDependencies()) {
- return Toast.makeText(context, text, duration);
+ context.getMainExecutor().execute(() ->
+ Toast.makeText(context, text, duration).show());
} else {
- return Toast.makeText(context, context.getMainLooper(), text, duration);
+ Toast.makeText(context, context.getMainLooper(), text, duration).show();
}
}
};
@@ -483,7 +481,6 @@
Manifest.permission.CONTROL_INCALL_EXPERIENCE, null);
// There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
- final UserManager userManager = UserManager.get(mContext);
mTelecomServiceImpl = new TelecomServiceImpl(
mContext, mCallsManager, mPhoneAccountRegistrar,
new CallIntentProcessor.AdapterImpl(defaultDialerCache),
@@ -491,7 +488,7 @@
@Override
public UserCallIntentProcessor create(Context context,
UserHandle userHandle) {
- return new UserCallIntentProcessor(context, userHandle);
+ return new UserCallIntentProcessor(context, userHandle, featureFlags);
}
},
defaultDialerCache,
@@ -534,4 +531,8 @@
public boolean isBootComplete() {
return mIsBootComplete;
}
+
+ public FeatureFlags getFeatureFlags() {
+ return mFeatureFlags;
+ }
}
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
index 7eb08d7..0a7dc19 100644
--- a/src/com/android/server/telecom/TelephonyUtil.java
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -74,7 +74,7 @@
TelephonyManager tm = (TelephonyManager) context.getSystemService(
Context.TELEPHONY_SERVICE);
return handle != null && tm.isEmergencyNumber(handle.getSchemeSpecificPart());
- } catch (IllegalStateException ise) {
+ } catch (UnsupportedOperationException | IllegalStateException ignored) {
return false;
}
}
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index abc7ff6..0ed71df 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -61,6 +61,14 @@
return Timeouts.getEmergencyCallbackWindowMillis(cr);
}
+ public long getEmergencyCallTimeoutMillis(ContentResolver cr) {
+ return Timeouts.getEmergencyCallTimeoutMillis(cr);
+ }
+
+ public long getEmergencyCallTimeoutRadioOffMillis(ContentResolver cr) {
+ return Timeouts.getEmergencyCallTimeoutRadioOffMillis(cr);
+ }
+
public long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver cr) {
return Timeouts.getUserDefinedCallRedirectionTimeoutMillis(cr);
}
@@ -127,7 +135,6 @@
public int getDaysBackToSearchEmergencyDiagnosticEntries(){
return Timeouts.getDaysBackToSearchEmergencyDiagnosticEntries();
-
}
}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d497c6a..50ef2e8 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -21,6 +21,7 @@
import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
import android.content.ComponentName;
+import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.OutcomeReceiver;
@@ -62,7 +63,7 @@
* on a per-client basis which is tied to a {@link PhoneAccountHandle}
*/
public class TransactionalServiceWrapper implements
- ConnectionServiceFocusManager.ConnectionServiceFocus {
+ ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
private static final String TAG = TransactionalServiceWrapper.class.getSimpleName();
// CallControl : Client (ex. voip app) --> Telecom
@@ -185,10 +186,12 @@
@Override
public void setActive(String callId, android.os.ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sA");
createTransactions(callId, callback, SET_ACTIVE);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -196,10 +199,12 @@
@Override
public void answer(int videoState, String callId, android.os.ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.a");
createTransactions(callId, callback, ANSWER, videoState);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -207,10 +212,12 @@
@Override
public void setInactive(String callId, android.os.ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sI");
createTransactions(callId, callback, SET_INACTIVE);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -219,10 +226,12 @@
public void disconnect(String callId, DisconnectCause disconnectCause,
android.os.ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.d");
createTransactions(callId, callback, DISCONNECT, disconnectCause);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -230,11 +239,13 @@
@Override
public void setMuteState(boolean isMuted, android.os.ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sMS");
addTransactionsToManager(
new SetMuteStateTransaction(mCallsManager, isMuted), callback);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -242,10 +253,12 @@
@Override
public void startCallStreaming(String callId, android.os.ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sCS");
createTransactions(callId, callback, START_STREAMING);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -253,10 +266,12 @@
@Override
public void requestVideoState(int videoState, String callId, ResultReceiver callback)
throws RemoteException {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.rVS");
createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -310,7 +325,8 @@
// This request is originating from the VoIP application.
private void handleCallControlNewCallFocusTransactions(Call call, String action,
boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
- mTransactionManager.addTransaction(createSetActiveTransactions(call),
+ mTransactionManager.addTransaction(
+ createSetActiveTransactions(call, true /* isCallControlRequest */),
new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
@@ -334,11 +350,13 @@
@Override
public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.rCEC");
addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
callback);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -348,6 +366,7 @@
*/
@Override
public void sendEvent(String callId, String event, Bundle extras) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sE");
Call call = mTrackedCalls.get(callId);
@@ -359,6 +378,7 @@
+ "found. Most likely the call has been disconnected");
}
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -426,7 +446,8 @@
Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
- SerialTransaction serialTransactions = createSetActiveTransactions(call);
+ SerialTransaction serialTransactions = createSetActiveTransactions(call,
+ false /* isCallControlRequest */);
// 3. get ack from client (that the requested call can go active)
if (isAnswerRequest) {
serialTransactions.appendTransaction(
@@ -552,6 +573,7 @@
}
}
+ @Override
public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
if (call != null) {
try {
@@ -561,6 +583,7 @@
}
}
+ @Override
public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
if (call != null) {
try {
@@ -571,6 +594,7 @@
}
}
+ @Override
public void onMuteStateChanged(Call call, boolean isMuted) {
if (call != null) {
try {
@@ -580,6 +604,7 @@
}
}
+ @Override
public void onVideoStateChanged(Call call, int videoState) {
if (call != null) {
try {
@@ -631,12 +656,13 @@
mCallsManager.removeCall(call);
}
- private SerialTransaction createSetActiveTransactions(Call call) {
+ private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
// create list for multiple transactions
List<VoipCallTransaction> transactions = new ArrayList<>();
// potentially hold the current active call in order to set a new call (active/answered)
- transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
+ transactions.add(
+ new MaybeHoldCallForNewCallTransaction(mCallsManager, call, isCallControlRequest));
// And request a new focus call update
transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index 670ad34..57906d4 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -36,18 +36,35 @@
}
private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
- UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ UserManager userManager = context.getSystemService(UserManager.class);
return userManager.getUserInfo(userHandle.getIdentifier());
}
- public static boolean isManagedProfile(Context context, UserHandle userHandle) {
+ public static boolean isManagedProfile(Context context, UserHandle userHandle,
+ FeatureFlags featureFlags) {
+ UserManager userManager = context.createContextAsUser(userHandle, 0)
+ .getSystemService(UserManager.class);
UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
- return userInfo != null && userInfo.isManagedProfile();
+ return featureFlags.telecomResolveHiddenDependencies()
+ ? userManager != null && userManager.isManagedProfile()
+ : userInfo != null && userInfo.isManagedProfile();
}
- public static boolean isProfile(Context context, UserHandle userHandle) {
+ public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
+ UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
+ UserManager.class);
+ return um != null && um.isPrivateProfile();
+ }
+
+ public static boolean isProfile(Context context, UserHandle userHandle,
+ FeatureFlags featureFlags) {
+ UserManager userManager = context.createContextAsUser(userHandle, 0)
+ .getSystemService(UserManager.class);
UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
- return userInfo != null && userInfo.profileGroupId != userInfo.id;
+ return featureFlags.telecomResolveHiddenDependencies()
+ ? userManager != null && userManager.isProfile()
+ : userInfo != null && userInfo.profileGroupId != userInfo.id
+ && userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
}
public static void showErrorDialogForRestrictedOutgoingCall(Context context,
@@ -61,7 +78,8 @@
}
public static boolean hasOutgoingCallsUserRestriction(Context context,
- UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag) {
+ UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag,
+ FeatureFlags featureFlags) {
// Set handle for conference calls. Refer to {@link Connection#ADHOC_CONFERENCE_ADDRESS}.
if (handle == null) {
handle = Uri.parse("tel:conf-factory");
@@ -71,20 +89,23 @@
// 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)) {
+ if (!UserUtil.isManagedProfile(context, userHandle, featureFlags)) {
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ boolean hasUserRestriction = featureFlags.telecomResolveHiddenDependencies()
+ ? userManager.hasUserRestrictionForUser(
+ UserManager.DISALLOW_OUTGOING_CALLS, userHandle)
+ : userManager.hasUserRestriction(
+ UserManager.DISALLOW_OUTGOING_CALLS, 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)) {
+ } else if (hasUserRestriction) {
final DevicePolicyManager dpm =
context.getSystemService(DevicePolicyManager.class);
if (dpm == null) {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index a0ffe63..0a8ce5a 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -16,6 +16,11 @@
package com.android.server.telecom.bluetooth;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -31,19 +36,25 @@
import android.telecom.Log;
import android.util.ArraySet;
import android.util.LocalLog;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.AudioRoute;
import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.flags.FeatureFlags;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -58,6 +69,16 @@
public static final int DEVICE_TYPE_HEARING_AID = 1;
public static final int DEVICE_TYPE_LE_AUDIO = 2;
+ private static final Map<Integer, Integer> PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>();
+ static {
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET,
+ AudioRoute.TYPE_BLUETOOTH_SCO);
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO,
+ AudioRoute.TYPE_BLUETOOTH_LE);
+ PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID,
+ TYPE_BLUETOOTH_HA);
+ }
+
private BluetoothLeAudio.Callback mLeAudioCallbacks =
new BluetoothLeAudio.Callback() {
@Override
@@ -112,15 +133,14 @@
+ mBluetoothHearingAid;
} else if (profile == BluetoothProfile.LE_AUDIO) {
mBluetoothLeAudioService = (BluetoothLeAudio) proxy;
- logString = "Got BluetoothLeAudio: "
- + mBluetoothLeAudioService;
+ logString = ("Got BluetoothLeAudio: " + mBluetoothLeAudioService )
+ + (", mLeAudioCallbackRegistered: "
+ + mLeAudioCallbackRegistered);
if (!mLeAudioCallbackRegistered) {
- try {
- mBluetoothLeAudioService.registerCallback(
- mExecutor, mLeAudioCallbacks);
- mLeAudioCallbackRegistered = true;
- } catch (IllegalStateException e) {
- logString += ", but Bluetooth is down";
+ if (mFeatureFlags.postponeRegisterToLeaudio()) {
+ mExecutor.execute(this::registerToLeAudio);
+ } else {
+ registerToLeAudio();
}
}
} else {
@@ -135,6 +155,29 @@
}
}
+ private void registerToLeAudio() {
+ synchronized (mLock) {
+ String logString = "Register to leAudio";
+
+ if (mLeAudioCallbackRegistered) {
+ logString += ", but already registered";
+ Log.i(BluetoothDeviceManager.this, logString);
+ mLocalLog.log(logString);
+ return;
+ }
+ try {
+ mLeAudioCallbackRegistered = true;
+ mBluetoothLeAudioService.registerCallback(
+ mExecutor, mLeAudioCallbacks);
+ } catch (IllegalStateException e) {
+ mLeAudioCallbackRegistered = false;
+ logString += ", but failed: " + e;
+ }
+ Log.i(BluetoothDeviceManager.this, logString);
+ mLocalLog.log(logString);
+ }
+ }
+
@Override
public void onServiceDisconnected(int profile) {
Log.startSession("BPSL.oSD");
@@ -172,11 +215,15 @@
Log.i(BluetoothDeviceManager.this, logString);
mLocalLog.log(logString);
- List<BluetoothDevice> devicesToRemove = new LinkedList<>(
- lostServiceDevices.values());
- lostServiceDevices.clear();
- for (BluetoothDevice device : devicesToRemove) {
- mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ handleAudioRefactoringServiceDisconnected(profile);
+ } else {
+ List<BluetoothDevice> devicesToRemove = new LinkedList<>(
+ lostServiceDevices.values());
+ lostServiceDevices.clear();
+ for (BluetoothDevice device : devicesToRemove) {
+ mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ }
}
}
} finally {
@@ -185,6 +232,34 @@
}
};
+ private void handleAudioRefactoringServiceDisconnected(int profile) {
+ CallAudioRouteController controller = (CallAudioRouteController)
+ mCallAudioRouteAdapter;
+ Map<AudioRoute, BluetoothDevice> btRoutes = controller
+ .getBluetoothRoutes();
+ List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
+ new ArrayList<>();
+ for (AudioRoute route: btRoutes.keySet()) {
+ if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+ continue;
+ }
+ BluetoothDevice device = btRoutes.get(route);
+ // Prevent concurrent modification exception by just iterating through keys instead of
+ // simultaneously removing them.
+ btRoutesToRemove.add(new Pair<>(route, device));
+ }
+
+ for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
+ btRoutesToRemove) {
+ AudioRoute route = routeToRemove.first;
+ BluetoothDevice device = routeToRemove.second;
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ BT_DEVICE_REMOVED, route.getType(), device);
+ }
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ SWITCH_BASELINE_ROUTE, 0, (String) null);
+ }
+
private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
new LinkedHashMap<>();
private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -223,6 +298,7 @@
private AudioManager mAudioManager;
private Executor mExecutor;
private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private CallAudioRouteAdapter mCallAudioRouteAdapter;
private FeatureFlags mFeatureFlags;
public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
@@ -489,12 +565,14 @@
}
}
- public void disconnectSco() {
+ public int disconnectSco() {
+ int result = BluetoothStatusCodes.ERROR_UNKNOWN;
if (getBluetoothHeadset() == null) {
Log.w(this, "Trying to disconnect audio but no headset service exists.");
} else {
- mBluetoothHeadset.disconnectAudio();
+ result = mBluetoothHeadset.disconnectAudio();
}
+ return result;
}
public boolean isLeAudioCommunicationDevice() {
@@ -651,6 +729,28 @@
return result;
}
+ public boolean setCommunicationDeviceForAddress(String address) {
+ AudioDeviceInfo deviceInfo = null;
+ List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+ if (devices.size() == 0) {
+ Log.w(this, " No communication devices available.");
+ return false;
+ }
+
+ for (AudioDeviceInfo device : devices) {
+ Log.i(this, " Available device type: " + device.getType());
+ if (device.getAddress().equals(address)) {
+ deviceInfo = device;
+ break;
+ }
+ }
+
+ if (!mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+ return mAudioManager.setCommunicationDevice(deviceInfo);
+ }
+ return true;
+ }
+
// Connect audio to the bluetooth device at address, checking to see whether it's
// le audio, hearing aid or a HFP device, and using the proper BT API.
public boolean connectAudio(String address, boolean switchingBtDevices) {
@@ -747,6 +847,54 @@
}
}
+ /**
+ * Used by CallAudioRouteController in order to connect the BT device.
+ * @param device {@link BluetoothDevice} to connect to.
+ * @param type {@link AudioRoute.AudioRouteType} associated with the device.
+ * @return {@code true} if device was successfully connected, {@code false} otherwise.
+ */
+ public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+ String address = device.getAddress();
+ int callProfile = BluetoothProfile.LE_AUDIO;
+ if (type == TYPE_BLUETOOTH_SCO) {
+ callProfile = BluetoothProfile.HEADSET;
+ } else if (type == TYPE_BLUETOOTH_HA) {
+ callProfile = BluetoothProfile.HEARING_AID;
+ }
+
+ Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+ if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+ && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+ Log.i(this, "Preferred duplex profile for device=" + address + " is "
+ + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+ callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ }
+
+ if (callProfile == BluetoothProfile.LE_AUDIO
+ || callProfile == BluetoothProfile.HEARING_AID) {
+ return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ } else if (callProfile == BluetoothProfile.HEADSET) {
+ boolean success = mBluetoothAdapter.setActiveDevice(device,
+ BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+ if (!success) {
+ Log.w(this, "Couldn't set active device to %s", address);
+ return false;
+ }
+ if (getBluetoothHeadset() != null) {
+ int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+ return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+ scoConnectionRequest
+ == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ } else {
+ Log.w(this, "Couldn't find bluetooth headset service");
+ return false;
+ }
+ } else {
+ Log.w(this, "Attempting to turn on audio for a disconnected device");
+ return false;
+ }
+ }
+
public void cacheHearingAidDevice() {
if (mBluetoothAdapter != null) {
for (BluetoothDevice device : mBluetoothAdapter.getActiveDevices(
@@ -791,6 +939,10 @@
}
}
+ public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+ mCallAudioRouteAdapter = adapter;
+ }
+
public void dump(IndentingPrintWriter pw) {
mLocalLog.dump(pw);
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 7da5339..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -28,6 +28,7 @@
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -40,14 +41,12 @@
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;
@@ -167,10 +166,23 @@
removeDevice((String) args.arg2);
break;
case CONNECT_BT:
- String actualAddress = connectBtAudio((String) args.arg2,
- false /* switchingBtDevices*/);
+ String actualAddress;
+ boolean connected;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> addressInfo = computeAddressToConnectTo(
+ (String) args.arg2, false, null);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ addressInfo = handleDeviceAlreadyConnected(addressInfo);
+ actualAddress = addressInfo.first;
+ connected = connectBtAudio(actualAddress, 0,
+ false /* switchingBtDevices*/);
+ } else {
+ actualAddress = connectBtAudioLegacy((String) args.arg2, false);
+ connected = actualAddress != null;
+ }
- if (actualAddress != null) {
+ if (connected) {
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioOff/CONNECT_BT"));
} else {
@@ -183,10 +195,24 @@
break;
case RETRY_BT_CONNECTION:
Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
- String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
- false /* switchingBtDevices*/);
+ String retryAddress;
+ boolean retrySuccessful;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+ (String) args.arg2, false, null);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+ retryAddress = retryAddressInfo.first;
+ retrySuccessful = connectBtAudio(retryAddress, args.argi1,
+ false /* switchingBtDevices*/);
+ } else {
+ retryAddress = connectBtAudioLegacy((String) args.arg2, args.argi1,
+ false /* switchingBtDevices*/);
+ retrySuccessful = retryAddress != null;
+ }
- if (retryAddress != null) {
+ if (retrySuccessful) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioOff/RETRY_BT_CONNECTION"));
} else {
@@ -257,7 +283,7 @@
String address = (String) args.arg2;
boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
- if (switchingBtDevices == true) { // check if it is an hearing aid pair
+ if (switchingBtDevices) { // check if it is an hearing aid pair
BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
if (bluetoothAdapter != null) {
List<BluetoothDevice> activeHearingAids =
@@ -274,7 +300,9 @@
}
}
}
-
+ }
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ switchingBtDevices &= (mDeviceAddress != null);
}
}
try {
@@ -290,14 +318,30 @@
}
break;
case CONNECT_BT:
+ String actualAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+ switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ addressInfo = handleDeviceAlreadyConnected(addressInfo);
+ actualAddress = addressInfo.first;
+ switchingBtDevices = addressInfo.second;
+ }
+
if (!switchingBtDevices) {
// Ignore repeated connection attempts to the same device
break;
}
- String actualAddress = connectBtAudio(address,
- true /* switchingBtDevices*/);
- if (actualAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ actualAddress = connectBtAudioLegacy(address,
+ true /* switchingBtDevices*/);
+ }
+ boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+ : actualAddress != null;
+ if (connected) {
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioConnecting/CONNECT_BT"));
} else {
@@ -309,14 +353,32 @@
mDeviceManager.disconnectAudio();
break;
case RETRY_BT_CONNECTION:
+ String retryAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+ address, switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+ retryAddress = retryAddressInfo.first;
+ switchingBtDevices = retryAddressInfo.second;
+ }
+
if (!switchingBtDevices) {
Log.d(LOG_TAG, "Retry message came through while connecting.");
break;
}
- String retryAddress = connectBtAudio(address, args.argi1,
- true /* switchingBtDevices*/);
- if (retryAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ retryAddress = connectBtAudioLegacy(address, args.argi1,
+ true /* switchingBtDevices*/);
+ }
+ boolean retrySuccessful = mFeatureFlags
+ .resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(retryAddress, args.argi1,
+ true /* switchingBtDevices*/)
+ : retryAddress != null;
+ if (retrySuccessful) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioConnecting/RETRY_BT_CONNECTION"));
} else {
@@ -395,6 +457,10 @@
SomeArgs args = (SomeArgs) msg.obj;
String address = (String) args.arg2;
boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ switchingBtDevices &= (mDeviceAddress != null);
+ }
+
try {
switch (msg.what) {
case NEW_DEVICE_CONNECTED:
@@ -407,6 +473,17 @@
}
break;
case CONNECT_BT:
+ String actualAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+ switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ addressInfo = handleDeviceAlreadyConnected(addressInfo);
+ actualAddress = addressInfo.first;
+ switchingBtDevices = addressInfo.second;
+ }
+
if (!switchingBtDevices) {
// Ignore connection to already connected device but still notify
// CallAudioRouteStateMachine since this might be a switch from other
@@ -415,9 +492,14 @@
break;
}
- String actualAddress = connectBtAudio(address,
- true /* switchingBtDevices*/);
- if (actualAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ actualAddress = connectBtAudioLegacy(address,
+ true /* switchingBtDevices*/);
+ }
+ boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+ : actualAddress != null;
+ if (connected) {
if (mFeatureFlags.useActualAddressToEnterConnectingState()) {
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioConnected/CONNECT_BT"));
@@ -434,14 +516,32 @@
mDeviceManager.disconnectAudio();
break;
case RETRY_BT_CONNECTION:
+ String retryAddress = null;
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+ address, switchingBtDevices, mDeviceAddress);
+ // See if we need to transition route if the device is already
+ // connected. If connected, another connection will not occur.
+ retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+ retryAddress = retryAddressInfo.first;
+ switchingBtDevices = retryAddressInfo.second;
+ }
+
if (!switchingBtDevices) {
Log.d(LOG_TAG, "Retry message came through while connected.");
break;
}
- String retryAddress = connectBtAudio(address, args.argi1,
- true /* switchingBtDevices*/);
- if (retryAddress != null) {
+ if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ retryAddress = connectBtAudioLegacy(address, args.argi1,
+ true /* switchingBtDevices*/);
+ }
+ boolean retrySuccessful = mFeatureFlags
+ .resolveSwitchingBtDevicesComputation()
+ ? connectBtAudio(retryAddress, args.argi1,
+ true /* switchingBtDevices*/)
+ : retryAddress != null;
+ if (retrySuccessful) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioConnected/RETRY_BT_CONNECTION"));
} else {
@@ -742,8 +842,124 @@
return false;
}
- private String connectBtAudio(String address, boolean switchingBtDevices) {
- return connectBtAudio(address, 0, switchingBtDevices);
+ /**
+ * Determines the address that should be used for the connection attempt. In the case that the
+ * specified address to be used is null, Telecom will try to find an arbitrary address to
+ * connect instead.
+ *
+ * @param address The address that should be prioritized for the connection attempt
+ * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+ * @param stateAddress The address stored in the state that indicates the connecting/connected
+ * device.
+ * @return {@link Pair} containing the address to connect to and whether an existing BT audio
+ * connection for a different device exists.
+ */
+ private Pair<String, Boolean> computeAddressToConnectTo(
+ String address, boolean switchingBtDevices, String stateAddress) {
+ Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
+ Optional<BluetoothDevice> matchingDevice = deviceList.stream()
+ .filter(d -> Objects.equals(d.getAddress(), address))
+ .findAny();
+
+ String actualAddress = matchingDevice.isPresent()
+ ? address : getActiveDeviceAddress();
+ if (actualAddress == null) {
+ Log.i(this, "No device specified and BT stack has no active device."
+ + " Using arbitrary device - except watch");
+ if (deviceList.size() > 0) {
+ 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 new Pair<>(null, false);
+ }
+ if (switchingBtDevices && actualAddress.equals(stateAddress)) {
+ switchingBtDevices = false;
+ }
+ }
+ if (!matchingDevice.isPresent()) {
+ Log.i(this, "No device with address %s available. Using %s instead.",
+ address, actualAddress);
+ }
+ return new Pair<>(actualAddress, switchingBtDevices);
+ }
+
+ /**
+ * Handles route switching to the connected state for a device. This currently handles the case
+ * for hearing aids when the route manager reports AudioOff since Telecom doesn't treat HA as
+ * the active device outside of a call.
+ *
+ * @param addressInfo A {@link Pair} containing the BT address to connect to as well as if we're
+ * handling a switch of BT devices.
+ * @return {@link Pair} indicating the address to connect to as well as if we're handling a
+ * switch of BT devices. If the device is already connected, then the
+ * return value will be {null, false} to indicate that a connection attempt
+ * is not required.
+ */
+ private Pair<String, Boolean> handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo) {
+ String address = addressInfo.first;
+ BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
+ if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
+ address)) {
+ Log.i(this, "trying to connect to already connected device -- skipping connection"
+ + " and going into the actual connected state.");
+ transitionToActualState();
+ return new Pair<>(null, false);
+ }
+ return addressInfo;
+ }
+
+ /**
+ * Initiates a connection to the BT address specified.
+ * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
+ * Telecom from within it.
+ * @param address The address that should be tried first. May be null.
+ * @param retryCount The number of times this connection attempt has been retried.
+ * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+ * @return {@code true} if the connection to the address was successful, otherwise {@code false}
+ * if the connection fails.
+ *
+ * Note: This should only be used in par with the resolveSwitchingBtDevicesComputation flag.
+ */
+ private boolean connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+ if (address == null) {
+ return false;
+ }
+
+ if (switchingBtDevices) {
+ /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
+ mDeviceManager.disconnectAudio();
+ }
+
+ if (!mDeviceManager.connectAudio(address, switchingBtDevices)) {
+ boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
+ Log.w(LOG_TAG, "Could not connect to %s. Will %s", address,
+ shouldRetry ? "retry" : "not retry");
+ if (shouldRetry) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = Log.createSubsession();
+ args.arg2 = address;
+ args.argi1 = retryCount + 1;
+ sendMessageDelayed(RETRY_BT_CONNECTION, args,
+ mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+ mContext.getContentResolver()));
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ private String connectBtAudioLegacy(String address, boolean switchingBtDevices) {
+ return connectBtAudioLegacy(address, 0, switchingBtDevices);
}
/**
@@ -756,7 +972,8 @@
* @return The address of the device that's actually being connected to, or null if no
* connection was successful.
*/
- private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+ private String connectBtAudioLegacy(String address, int retryCount,
+ boolean switchingBtDevices) {
Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
Optional<BluetoothDevice> matchingDevice = deviceList.stream()
.filter(d -> Objects.equals(d.getAddress(), address))
@@ -1048,4 +1265,8 @@
mHfpActiveDeviceCache = device;
}
}
+
+ public BluetoothDeviceManager getDeviceManager() {
+ return mDeviceManager;
+ }
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 6d80cd5..9168388 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -22,6 +22,7 @@
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
@@ -39,15 +40,16 @@
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
import com.android.internal.os.SomeArgs;
import com.android.server.telecom.AudioRoute;
import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;
-
public class BluetoothStateReceiver extends BroadcastReceiver {
private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
public static final IntentFilter INTENT_FILTER;
@@ -116,9 +118,28 @@
args.arg2 = device.getAddress();
switch (bluetoothHeadsetAudioState) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
- if (Flags.useRefactoredAudioRouteSwitching()) {
- mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
- device);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(true);
+ if (audioRouteController.isPending()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+ device);
+ } else {
+ // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED
+ // is sent later, indicating that SCO audio is on. We should route
+ // appropriately in order for the UI to reflect this state.
+ AudioRoute btRoute = audioRouteController.getBluetoothRoute(
+ AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
+ if (btRoute != null) {
+ audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
+ audioRouteController.overrideIsPending(true);
+ audioRouteController.getPendingAudioRoute()
+ .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+ }
+ }
} else {
if (!mIsInCall) {
Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
@@ -129,6 +150,9 @@
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
if (Flags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(false);
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
device);
} else {
@@ -218,6 +242,25 @@
} else {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
audioRouteType, device.getAddress());
+ if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
+ || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+ if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
+ device.getAddress())) {
+ Log.i(this, "handleActiveDeviceChanged: Failed to set "
+ + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+ + "pending audio route.", device);
+ mCallAudioRouteAdapter.getPendingAudioRoute()
+ .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ device.getAddress()), device.getAddress());
+ } else {
+ // Track the currently set communication device.
+ int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
+ ? AudioRoute.TYPE_BLUETOOTH_LE
+ : AudioRoute.TYPE_BLUETOOTH_HA;
+ mCallAudioRouteAdapter.getPendingAudioRoute()
+ .setCommunicationDeviceType(routeType);
+ }
+ }
}
} else {
mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
index 1fda542..55540de 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
@@ -19,12 +19,19 @@
import android.content.Context;
import android.os.Bundle;
import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import android.telecom.Log;
+import com.android.server.telecom.flags.FeatureFlags;
+
public class BlockCheckerAdapter {
private static final String TAG = BlockCheckerAdapter.class.getSimpleName();
- public BlockCheckerAdapter() { }
+ private FeatureFlags mFeatureFlags;
+
+ public BlockCheckerAdapter(FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
+ }
/**
* Returns the call blocking status for the {@code phoneNumber}.
@@ -32,7 +39,6 @@
* This method catches all underlying exceptions to ensure that this method never throws any
* exception.
*
- * @param context the context of the caller.
* @param phoneNumber the number to check.
* @param numberPresentation the presentation code associated with the call.
* @param isNumberInContacts indicates if the provided number exists as a contact.
@@ -48,10 +54,20 @@
int numberPresentation, boolean isNumberInContacts) {
int blockStatus = BlockedNumberContract.STATUS_NOT_BLOCKED;
long startTimeNano = System.nanoTime();
+ BlockedNumbersManager blockedNumbersManager = mFeatureFlags
+ .telecomMainlineBlockedNumbersManager()
+ ? context.getSystemService(BlockedNumbersManager.class)
+ : null;
try {
- blockStatus = BlockedNumberContract.BlockedNumbers.shouldSystemBlockNumber(
- context, phoneNumber, numberPresentation, isNumberInContacts);
+ Bundle extras = new Bundle();
+ extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, numberPresentation);
+ extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, isNumberInContacts);
+ blockStatus = blockedNumbersManager != null
+ ? blockedNumbersManager.shouldSystemBlockNumber(phoneNumber,
+ numberPresentation, isNumberInContacts)
+ : BlockedNumberContract.SystemContract.shouldSystemBlockNumber(context,
+ phoneNumber, extras);
if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
Log.d(TAG, phoneNumber + " is blocked.");
}
diff --git a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
index f640826..66137d5 100644
--- a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
@@ -20,10 +20,10 @@
/**
* Adapter interface that wraps methods from
- * {@link android.provider.BlockedNumberContract.BlockedNumbers} and
+ * {@link android.provider.BlockedNumbersManager} and
* {@link com.android.server.telecom.settings.BlockedNumbersUtil} to make things testable.
*/
public interface BlockedNumbersAdapter {
- boolean shouldShowEmergencyCallNotification (Context context);
+ boolean shouldShowEmergencyCallNotification(Context context);
void updateEmergencyCallNotification(Context context, boolean showNotification);
}
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
index 7a2d415..a6f089f 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
@@ -143,15 +143,25 @@
return PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart());
}
- protected Uri formatNumberToE164(Uri handle) {
+ @VisibleForTesting
+ public Uri formatNumberToE164(Uri handle) {
String number = handle.getSchemeSpecificPart();
// Format number to E164
TelephonyManager tm = (TelephonyManager) mContext.getSystemService(
Context.TELEPHONY_SERVICE);
Log.i(this, "formatNumberToE164, original number: " + Log.pii(number));
- number = PhoneNumberUtils.formatNumberToE164(number, tm.getNetworkCountryIso());
- Log.i(this, "formatNumberToE164, formatted E164 number: " + Log.pii(number));
+ String networkCountryIso;
+ try {
+ number = PhoneNumberUtils.formatNumberToE164(number, tm.getNetworkCountryIso());
+ Log.i(this, "formatNumberToE164, formatted E164 number: " + Log.pii(number));
+ } catch (UnsupportedOperationException ignored) {
+ // Note: Yes, this could default back to the system locale, however redirection when
+ // there is no telephony is NOT expected. Hence in reality we shouldn't really hit this
+ // code path in practice; this is a "just in case" to ensure we don't crash.
+ Log.w(this, "formatNumberToE164: no telephony; use original format");
+ number = null;
+ }
// if there is a problem with parsing the phone number, formatNumberToE164 will return null;
// and should just use the original number in that case.
if (number == null) {
diff --git a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
index b7e5880..b89fe94 100644
--- a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
+++ b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
@@ -61,9 +61,14 @@
return;
}
- String packageName = uri.getSchemeSpecificPart();
- handlePackageRemoved(context, packageName);
- handleUninstallOfCallScreeningService(context, packageName);
+ final PendingResult result = goAsync();
+ // Move computation off into a separate thread to prevent ANR.
+ new Thread(() -> {
+ String packageName = uri.getSchemeSpecificPart();
+ handlePackageRemoved(context, packageName);
+ handleUninstallOfCallScreeningService(context, packageName);
+ result.finish();
+ }).start();
}
}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 845f788..2d8c78e 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -27,6 +27,7 @@
import android.os.ServiceManager;
import android.os.SystemClock;
import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import android.telecom.Log;
import android.telecom.CallerInfoAsyncQuery;
@@ -104,6 +105,7 @@
static void initializeTelecomSystem(Context context,
InternalServiceRetrieverAdapter internalServiceRetriever) {
if (TelecomSystem.getInstance() == null) {
+ FeatureFlags featureFlags = new FeatureFlagsImpl();
NotificationChannelManager notificationChannelManager =
new NotificationChannelManager();
notificationChannelManager.createChannels(context);
@@ -223,8 +225,11 @@
@Override
public boolean shouldShowEmergencyCallNotification(Context
context) {
- return BlockedNumberContract.BlockedNumbers
- .shouldShowEmergencyCallNotification(context);
+ return featureFlags.telecomMainlineBlockedNumbersManager()
+ ? context.getSystemService(BlockedNumbersManager.class)
+ .shouldShowEmergencyCallNotification()
+ : BlockedNumberContract.SystemContract
+ .shouldShowEmergencyCallNotification(context);
}
@Override
@@ -234,7 +239,7 @@
showNotification);
}
},
- new FeatureFlagsImpl(),
+ featureFlags,
new com.android.internal.telephony.flags.FeatureFlagsImpl()));
}
}
diff --git a/src/com/android/server/telecom/components/UserCallActivity.java b/src/com/android/server/telecom/components/UserCallActivity.java
index d7b2001..6f5dfea 100644
--- a/src/com/android/server/telecom/components/UserCallActivity.java
+++ b/src/com/android/server/telecom/components/UserCallActivity.java
@@ -18,6 +18,8 @@
import com.android.server.telecom.CallIntentProcessor;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
import android.app.Activity;
import android.content.Context;
@@ -64,8 +66,13 @@
// See OutgoingCallBroadcaster in services/Telephony for more.
Intent intent = getIntent();
verifyCallAction(intent);
- final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
- final UserHandle userHandle = new UserHandle(userManager.getProcessUserId());
+ FeatureFlags featureFlags = getTelecomSystem() != null
+ ? getTelecomSystem().getFeatureFlags()
+ : new FeatureFlagsImpl();
+ final UserManager userManager = getSystemService(UserManager.class);
+ final UserHandle userHandle = new UserHandle(
+ featureFlags.telecomResolveHiddenDependencies()
+ ? UserHandle.myUserId() : userManager.getProcessUserId());
// Once control flow has passed to this activity, it is no longer guaranteed that we can
// accurately determine whether the calling package has the CALL_PHONE runtime permission.
// At this point in time we trust that the ActivityManager has already performed this
@@ -73,9 +80,9 @@
// Create a new instance of intent to avoid modifying the
// ActivityThread.ActivityClientRecord#intent directly.
// Modifying directly may be a potential risk when relaunching this activity.
- new UserCallIntentProcessor(this, userHandle).processIntent(new Intent(intent),
- getCallingPackage(), false, true /* hasCallAppOp*/,
- false /* isLocalInvocation */);
+ new UserCallIntentProcessor(this, userHandle, featureFlags)
+ .processIntent(new Intent(intent), getCallingPackage(), false,
+ true /* hasCallAppOp*/, false /* isLocalInvocation */);
} finally {
Log.endSession();
wakelock.release();
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
index 41232c2..c3dc963 100755
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -34,6 +34,7 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.TelephonyUtil;
import com.android.server.telecom.UserUtil;
+import com.android.server.telecom.flags.FeatureFlags;
// TODO: Needed for move to system service: import com.android.internal.R;
@@ -58,10 +59,13 @@
private final Context mContext;
private final UserHandle mUserHandle;
+ private FeatureFlags mFeatureFlags;
- public UserCallIntentProcessor(Context context, UserHandle userHandle) {
+ public UserCallIntentProcessor(Context context, UserHandle userHandle,
+ FeatureFlags featureFlags) {
mContext = context;
mUserHandle = userHandle;
+ mFeatureFlags = featureFlags;
}
/**
@@ -105,8 +109,8 @@
handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null);
}
- if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle,
- handle, isSelfManaged, UserCallIntentProcessor.class.getCanonicalName())) {
+ if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle, handle, isSelfManaged,
+ UserCallIntentProcessor.class.getCanonicalName(), mFeatureFlags)) {
return;
}
diff --git a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
index d96b3e1..6ca4d2a 100644
--- a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
+++ b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
@@ -23,7 +23,6 @@
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.BlockedNumberContract;
-import com.android.server.telecom.R;
/**
* Retained fragment that runs an async task to add a blocked number.
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index 819b270..edc8da6 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -34,6 +34,7 @@
import android.database.Cursor;
import android.os.Bundle;
import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import android.telephony.PhoneNumberFormattingTextWatcher;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
@@ -53,7 +54,10 @@
import android.widget.TextView;
import android.widget.Toast;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
/**
@@ -75,8 +79,10 @@
BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " NOTNULL) AND (" +
BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " != '' ))";
+ private BlockedNumbersManager mBlockedNumbersManager;
private BlockNumberTaskFragment mBlockNumberTaskFragment;
private BlockedNumbersAdapter mAdapter;
+ private FeatureFlags mFeatureFlags;
private TextView mAddButton;
private ProgressBar mProgressBar;
private RelativeLayout mButterBar;
@@ -114,6 +120,7 @@
return;
}
+ mFeatureFlags = new FeatureFlagsImpl();
FragmentManager fm = getFragmentManager();
mBlockNumberTaskFragment =
(BlockNumberTaskFragment) fm.findFragmentByTag(TAG_BLOCK_NUMBER_TASK_FRAGMENT);
@@ -155,12 +162,15 @@
}
};
IntentFilter blockStatusIntentFilter = new IntentFilter(
- BlockedNumberContract.BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+ BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
Context.RECEIVER_EXPORTED);
getLoaderManager().initLoader(0, null, this);
+ mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+ ? getSystemService(BlockedNumbersManager.class)
+ : null;
}
@Override
@@ -183,8 +193,10 @@
}
private void updateButterBar() {
- if (BlockedNumberContract.BlockedNumbers
- .getBlockSuppressionStatus(this).getIsSuppressed()) {
+ boolean isBlockSuppressionEnabled = mBlockedNumbersManager != null
+ ? mBlockedNumbersManager.getBlockSuppressionStatus().getIsSuppressed()
+ : BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed;
+ if (isBlockSuppressionEnabled) {
mButterBar.setVisibility(View.VISIBLE);
} else {
mButterBar.setVisibility(View.GONE);
@@ -239,7 +251,11 @@
if (view == mAddButton) {
showAddBlockedNumberDialog();
} else if (view == mReEnableButton) {
- BlockedNumberContract.BlockedNumbers.endBlockSuppression(this);
+ if (mBlockedNumbersManager != null) {
+ mBlockedNumbersManager.endBlockSuppression();
+ } else {
+ BlockedNumberContract.SystemContract.endBlockSuppression(this);
+ }
mButterBar.setVisibility(View.GONE);
}
}
@@ -302,12 +318,13 @@
}
}
- private boolean isEmergencyNumber(Context context, String number) {
+ @VisibleForTesting
+ public static boolean isEmergencyNumber(Context context, String number) {
try {
TelephonyManager tm = (TelephonyManager) context.getSystemService(
Context.TELEPHONY_SERVICE);
return tm.isEmergencyNumber(number);
- } catch (IllegalStateException ise) {
+ } catch (UnsupportedOperationException | IllegalStateException ignored) {
return false;
}
}
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
index e0fe81e..3e1da17 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
@@ -23,7 +23,8 @@
import android.content.Intent;
import android.os.PersistableBundle;
import android.os.UserHandle;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.text.BidiFormatter;
@@ -34,6 +35,7 @@
import com.android.server.telecom.R;
import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.ui.NotificationChannelManager;
import java.util.Locale;
@@ -148,8 +150,11 @@
* @return If {@code true} means the key enabled in the SharedPreferences,
* {@code false} otherwise.
*/
- public static boolean getBlockedNumberSetting(Context context, String key) {
- return BlockedNumbers.getBlockedNumberSetting(context, key);
+ public static boolean getBlockedNumberSetting(Context context, String key,
+ FeatureFlags featureFlags) {
+ return featureFlags.telecomMainlineBlockedNumbersManager()
+ ? context.getSystemService(BlockedNumbersManager.class).getBlockedNumberSetting(key)
+ : BlockedNumberContract.SystemContract.getEnhancedBlockSetting(context, key);
}
/**
@@ -159,7 +164,13 @@
* @param key preference key of SharedPreferences.
* @param value the register value to the SharedPreferences.
*/
- public static void setBlockedNumberSetting(Context context, String key, boolean value) {
- BlockedNumbers.setBlockedNumberSetting(context, key, value);
+ public static void setBlockedNumberSetting(Context context, String key, boolean value,
+ FeatureFlags featureFlags) {
+ if (featureFlags.telecomMainlineBlockedNumbersManager()) {
+ context.getSystemService(BlockedNumbersManager.class).setBlockedNumberSetting(key,
+ value);
+ } else {
+ BlockedNumberContract.SystemContract.setEnhancedBlockSetting(context, key, value);
+ }
}
}
diff --git a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
index 35b7f70..cc66a2d 100644
--- a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
+++ b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
@@ -20,19 +20,23 @@
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
/**
* Shows a dialog when user taps an notification in notification tray.
*/
public class CallBlockDisabledActivity extends Activity {
private AlertDialog mDialog;
+ private FeatureFlags mFeatureFlags;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mFeatureFlags = new FeatureFlagsImpl();
showCallBlockingOffDialog();
}
@@ -60,9 +64,9 @@
public void onClick(DialogInterface dialog, int which) {
BlockedNumbersUtil.setBlockedNumberSetting(
CallBlockDisabledActivity.this,
- BlockedNumberContract.BlockedNumbers
+ BlockedNumbersManager
.ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION,
- false);
+ false, mFeatureFlags);
BlockedNumbersUtil.updateEmergencyCallNotification(
CallBlockDisabledActivity.this, false);
finish();
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index 7ea8926..b54e273 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,7 +23,7 @@
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumbersManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telecom.Log;
@@ -32,6 +32,8 @@
import android.view.ViewGroup;
import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
public class EnhancedCallBlockingFragment extends PreferenceFragment
implements Preference.OnPreferenceChangeListener {
@@ -45,22 +47,25 @@
"block_unavailable_calls_setting";
private boolean mIsCombiningRestrictedAndUnknownOption = false;
private boolean mIsCombiningUnavailableAndUnknownOption = false;
+ private FeatureFlags mFeatureFlags;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.enhanced_call_blocking_settings);
+ mFeatureFlags = new FeatureFlagsImpl();
maybeConfigureCallBlockingOptions();
- setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
- setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
- setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
- setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
- setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+ setOnPreferenceChangeListener(
+ BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+ setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+ setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+ setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+ setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
if (!showPayPhoneBlocking()) {
Preference payPhoneOption = getPreferenceScreen()
- .findPreference(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+ .findPreference(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
getPreferenceScreen().removePreference(payPhoneOption);
}
}
@@ -122,13 +127,13 @@
public void onResume() {
super.onResume();
- updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
- updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
if (showPayPhoneBlocking()) {
- updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
}
- updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
- updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
}
/**
@@ -137,7 +142,8 @@
private void updateEnhancedBlockPref(String key) {
SwitchPreference pref = (SwitchPreference) findPreference(key);
if (pref != null) {
- pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(getActivity(), key));
+ pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(
+ getActivity(), key, mFeatureFlags));
}
}
@@ -148,18 +154,18 @@
Log.i(this, "onPreferenceChange: changing %s and %s to %b",
preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
- BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+ BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
}
if (mIsCombiningUnavailableAndUnknownOption) {
Log.i(this, "onPreferenceChange: changing %s and %s to %b",
preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
- BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+ BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
}
}
BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
- (boolean) objValue);
+ (boolean) objValue, mFeatureFlags);
return true;
}
diff --git a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
index 1604285..04228c1 100644
--- a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
+++ b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
@@ -41,6 +41,7 @@
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
@@ -285,12 +286,18 @@
* network location. If the network location does not exist, fall back to the locale
* setting.
*/
- private String getCurrentCountryIso(Context context) {
+ @VisibleForTesting
+ public String getCurrentCountryIso(Context context) {
// Without framework function calls, this seems to be the most accurate location service
// we can rely on.
final TelephonyManager telephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+ String countryIso;
+ try {
+ countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+ } catch (UnsupportedOperationException ignored) {
+ countryIso = null;
+ }
if (countryIso == null) {
countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index 25ce0ca..220b44e 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -59,6 +59,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.Constants;
@@ -506,12 +507,18 @@
* network location. If the network location does not exist, fall back to the locale
* setting.
*/
- private String getCurrentCountryIso(Context context) {
+ @VisibleForTesting
+ public String getCurrentCountryIso(Context context) {
// Without framework function calls, this seems to be the most accurate location service
// we can rely on.
final TelephonyManager telephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+ String countryIso;
+ try {
+ countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+ } catch (UnsupportedOperationException ignored) {
+ countryIso = null;
+ }
if (countryIso == null) {
countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/ToastFactory.java b/src/com/android/server/telecom/ui/ToastFactory.java
index 75b69da..1561f0b 100644
--- a/src/com/android/server/telecom/ui/ToastFactory.java
+++ b/src/com/android/server/telecom/ui/ToastFactory.java
@@ -21,6 +21,6 @@
import android.widget.Toast;
public interface ToastFactory {
- Toast makeText(Context context, @StringRes int resId, int duration);
- Toast makeText(Context context, CharSequence text, int duration);
+ void makeText(Context context, @StringRes int resId, int duration);
+ void makeText(Context context, CharSequence text, int duration);
}
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
index 93d9836..9e140a7 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -125,7 +125,7 @@
try {
// wait for the client to ack that CallEventCallback
- boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+ boolean success = latch.await(mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
if (!success) {
// client send onError and failed to complete transaction
Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
index e037a79..6841fcf 100644
--- a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import android.os.ResultReceiver;
import android.telecom.CallEndpoint;
+import android.telecom.CallException;
import android.util.Log;
import com.android.server.telecom.CallsManager;
@@ -49,8 +50,9 @@
future.complete(new VoipCallTransactionResult(
VoipCallTransactionResult.RESULT_SUCCEED, null));
} else {
+ // TODO:: define errors in CallException class. b/335703584
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, null));
+ CallException.CODE_ERROR_UNKNOWN, null));
}
}
});
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index d35030c..ed0c7d6 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -19,14 +19,18 @@
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
import android.os.Bundle;
import android.telecom.CallAttributes;
import android.telecom.CallException;
import android.telecom.TelecomManager;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -38,19 +42,25 @@
private final CallAttributes mCallAttributes;
private final CallsManager mCallsManager;
private final Bundle mExtras;
+ private FeatureFlags mFeatureFlags;
+
+ public void setFeatureFlags(FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
+ }
public IncomingCallTransaction(String callId, CallAttributes callAttributes,
- CallsManager callsManager, Bundle extras) {
+ CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
super(callsManager.getLock());
mExtras = extras;
mCallId = callId;
mCallAttributes = callAttributes;
mCallsManager = callsManager;
+ mFeatureFlags = featureFlags;
}
public IncomingCallTransaction(String callId, CallAttributes callAttributes,
- CallsManager callsManager) {
- this(callId, callAttributes, callsManager, new Bundle());
+ CallsManager callsManager, FeatureFlags featureFlags) {
+ this(callId, callAttributes, callsManager, new Bundle(), featureFlags);
}
@Override
@@ -77,10 +87,19 @@
}
}
- private Bundle generateExtras(CallAttributes callAttributes) {
+ @VisibleForTesting
+ public Bundle generateExtras(CallAttributes callAttributes) {
mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
- mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+ if (mFeatureFlags.transactionalVideoState()) {
+ // Transactional calls need to remap the CallAttributes video state to the existing
+ // VideoProfile for consistency.
+ mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+ TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+ } else {
+ mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+ callAttributes.getCallType());
+ }
mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
callAttributes.getAddress());
diff --git a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
index a245c1c..3bed088 100644
--- a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
@@ -26,16 +26,23 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
+/**
+ * This VoipCallTransaction is responsible for holding any active call in favor of a new call
+ * request. If the active call cannot be held or disconnected, the transaction will fail.
+ */
public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
private final CallsManager mCallsManager;
private final Call mCall;
+ private final boolean mIsCallControlRequest;
- public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call) {
+ public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call,
+ boolean isCallControlRequest) {
super(callsManager.getLock());
mCallsManager = callsManager;
mCall = call;
+ mIsCallControlRequest = isCallControlRequest;
}
@Override
@@ -43,7 +50,8 @@
Log.d(TAG, "processTransaction");
CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, new OutcomeReceiver<>() {
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
+ new OutcomeReceiver<>() {
@Override
public void onResult(Boolean result) {
Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index b2625e6..8c970db 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -21,6 +21,8 @@
import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -29,9 +31,11 @@
import android.telecom.TelecomManager;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -45,9 +49,14 @@
private final CallAttributes mCallAttributes;
private final CallsManager mCallsManager;
private final Bundle mExtras;
+ private FeatureFlags mFeatureFlags;
+
+ public void setFeatureFlags(FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
+ }
public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
- CallsManager callsManager, Bundle extras) {
+ CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
super(callsManager.getLock());
mCallId = callId;
mContext = context;
@@ -55,11 +64,12 @@
mCallsManager = callsManager;
mExtras = extras;
mCallingPackage = mContext.getOpPackageName();
+ mFeatureFlags = featureFlags;
}
public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
- CallsManager callsManager) {
- this(callId, context, callAttributes, callsManager, new Bundle());
+ CallsManager callsManager, FeatureFlags featureFlags) {
+ this(callId, context, callAttributes, callsManager, new Bundle(), featureFlags);
}
@Override
@@ -121,12 +131,20 @@
}
}
- private Bundle generateExtras(CallAttributes callAttributes) {
+ @VisibleForTesting
+ public Bundle generateExtras(CallAttributes callAttributes) {
mExtras.setDefusable(true);
mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
- mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
- callAttributes.getCallType());
+ if (mFeatureFlags.transactionalVideoState()) {
+ // Transactional calls need to remap the CallAttributes video state to the existing
+ // VideoProfile for consistency.
+ mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+ } else {
+ mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ callAttributes.getCallType());
+ }
mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
return mExtras;
}
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 621892a..e235ead 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.voip;
+import android.telecom.CallException;
+
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
@@ -33,78 +35,56 @@
}
@Override
- public void start() {
- if (mStats != null) mStats.markStarted();
- // post timeout work
- CompletableFuture<Void> future = new CompletableFuture<>();
- mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
- future.thenApplyAsync((x) -> {
- if (mCompleted.getAndSet(true)) {
- return null;
- }
- if (mCompleteListener != null) {
- mCompleteListener.onTransactionTimeout(mTransactionName);
- }
- timeout();
- return null;
- }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
- + ".s", mLock));
+ public void processTransactions() {
+ if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+ scheduleTransaction();
+ return;
+ }
+ TransactionManager.TransactionCompleteListener subTransactionListener =
+ new TransactionManager.TransactionCompleteListener() {
+ private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
- if (mSubTransactions != null && mSubTransactions.size() > 0) {
- TransactionManager.TransactionCompleteListener subTransactionListener =
- new TransactionManager.TransactionCompleteListener() {
- private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
-
- @Override
- public void onTransactionCompleted(VoipCallTransactionResult result,
- String transactionName) {
- if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
- CompletableFuture.completedFuture(null).thenApplyAsync(
- (x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format(
- "sub transaction %s failed",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish(mainResult);
- return null;
- }, new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode()
- + ".oTC", mLock));
- } else {
- if (mCount.decrementAndGet() == 0) {
- scheduleTransaction();
- }
- }
- }
-
- @Override
- public void onTransactionTimeout(String transactionName) {
+ @Override
+ public void onTransactionCompleted(VoipCallTransactionResult result,
+ String transactionName) {
+ if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
CompletableFuture.completedFuture(null).thenApplyAsync(
(x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s timed out",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
+ finish(result);
+ mCompleteListener.onTransactionCompleted(result,
mTransactionName);
- finish(mainResult);
return null;
}, new LoggedHandlerExecutor(mHandler,
mTransactionName + "@" + hashCode()
- + ".oTT", mLock));
+ + ".oTC", mLock));
+ } else {
+ if (mCount.decrementAndGet() == 0) {
+ scheduleTransaction();
+ }
}
- };
- for (VoipCallTransaction transaction : mSubTransactions) {
- transaction.setCompleteListener(subTransactionListener);
- transaction.start();
- }
- } else {
- scheduleTransaction();
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName) {
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ CallException.CODE_OPERATION_TIMED_OUT,
+ String.format("sub transaction %s timed out",
+ transactionName));
+ finish(mainResult);
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTT", mLock));
+ }
+ };
+ for (VoipCallTransaction transaction : mSubTransactions) {
+ transaction.setCompleteListener(subTransactionListener);
+ transaction.start();
}
}
}
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
index f586cc3..e3aed8e 100644
--- a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
@@ -17,7 +17,6 @@
package com.android.server.telecom.voip;
import android.os.OutcomeReceiver;
-import android.telecom.CallAttributes;
import android.telecom.CallException;
import android.util.Log;
@@ -25,6 +24,7 @@
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.flags.Flags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -69,7 +69,8 @@
return future;
}
- if (mCallsManager.getActiveCall() != null) {
+ if (!Flags.transactionalHoldDisconnectsUnholdable() &&
+ mCallsManager.getActiveCall() != null) {
future.complete(new VoipCallTransactionResult(
CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
"Already an active call. Request hold on current active call."));
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
index 64596b1..c1bc343 100644
--- a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -18,6 +18,7 @@
import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import android.telecom.CallException;
import android.telecom.VideoProfile;
import android.util.Log;
@@ -48,13 +49,8 @@
if (isRequestingVideoTransmission(mVideoProfileState) &&
!mCall.isVideoCallingSupportedByPhoneAccount()) {
future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
"Video calling is not supported by the target account"));
- } else if (isRequestingVideoTransmission(mVideoProfileState) &&
- !mCall.isTransactionalCallSupportsVideoCalling()) {
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- "Video calling is not supported according to the callAttributes"));
} else {
mCall.setVideoState(mVideoProfileState);
future.complete(new VoipCallTransactionResult(
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 7d5a178..748f285 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.voip;
+import android.telecom.CallException;
+
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
@@ -37,86 +39,65 @@
}
@Override
- public void start() {
- if (mStats != null) mStats.markStarted();
- // post timeout work
- CompletableFuture<Void> future = new CompletableFuture<>();
- mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
- future.thenApplyAsync((x) -> {
- if (mCompleted.getAndSet(true)) {
- return null;
- }
- if (mCompleteListener != null) {
- mCompleteListener.onTransactionTimeout(mTransactionName);
- }
- timeout();
- return null;
- }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
- + ".s", mLock));
+ public void processTransactions() {
+ if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+ scheduleTransaction();
+ return;
+ }
+ TransactionManager.TransactionCompleteListener subTransactionListener =
+ new TransactionManager.TransactionCompleteListener() {
+ private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
- if (mSubTransactions != null && mSubTransactions.size() > 0) {
- TransactionManager.TransactionCompleteListener subTransactionListener =
- new TransactionManager.TransactionCompleteListener() {
- private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
-
- @Override
- public void onTransactionCompleted(VoipCallTransactionResult result,
- String transactionName) {
- if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
- handleTransactionFailure();
- CompletableFuture.completedFuture(null).thenApplyAsync(
- (x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format(
- "sub transaction %s failed",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish(mainResult);
- return null;
- }, new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode()
- + ".oTC", mLock));
- } else {
- int currTransactionIndex = mTransactionIndex.incrementAndGet();
- if (currTransactionIndex < mSubTransactions.size()) {
- VoipCallTransaction transaction = mSubTransactions.get(
- currTransactionIndex);
- transaction.setCompleteListener(this);
- transaction.start();
- } else {
- scheduleTransaction();
- }
- }
- }
-
- @Override
- public void onTransactionTimeout(String transactionName) {
+ @Override
+ public void onTransactionCompleted(VoipCallTransactionResult result,
+ String transactionName) {
+ if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
handleTransactionFailure();
CompletableFuture.completedFuture(null).thenApplyAsync(
(x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s timed out",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
+ finish(result);
+ mCompleteListener.onTransactionCompleted(result,
mTransactionName);
- finish(mainResult);
return null;
}, new LoggedHandlerExecutor(mHandler,
mTransactionName + "@" + hashCode()
- + ".oTT", mLock));
+ + ".oTC", mLock));
+ } else {
+ int currTransactionIndex = mTransactionIndex.incrementAndGet();
+ if (currTransactionIndex < mSubTransactions.size()) {
+ VoipCallTransaction transaction = mSubTransactions.get(
+ currTransactionIndex);
+ transaction.setCompleteListener(this);
+ transaction.start();
+ } else {
+ scheduleTransaction();
+ }
}
- };
- VoipCallTransaction transaction = mSubTransactions.get(0);
- transaction.setCompleteListener(subTransactionListener);
- transaction.start();
- } else {
- scheduleTransaction();
- }
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName) {
+ handleTransactionFailure();
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ CallException.CODE_OPERATION_TIMED_OUT,
+ String.format("sub transaction %s timed out",
+ transactionName));
+ finish(mainResult);
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTT", mLock));
+ }
+ };
+ VoipCallTransaction transaction = mSubTransactions.get(0);
+ transaction.setCompleteListener(subTransactionListener);
+ transaction.start();
+
}
public void handleTransactionFailure() {}
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 299bcc3..0086d07 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -146,8 +146,8 @@
pendingTransactions = new ArrayList<>(mTransactions);
}
for (VoipCallTransaction t : pendingTransactions) {
- t.finish(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
- "clear called"));
+ t.finish(new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN
+ /* TODO:: define error b/335703584 */, "clear called"));
}
}
diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
index b17dedd..5de4b1d 100644
--- a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
@@ -18,14 +18,12 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
-import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.TelecomSystem;
-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
@@ -35,37 +33,30 @@
*/
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 static final long CALL_STATE_TIMEOUT_MILLISECONDS = 2000L;
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() {
+ private final 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);
+ mTransactionResult.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, TAG));
}
// 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;
+ public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock, Call call,
+ int targetCallState) {
+ super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
mCall = call;
mTargetCallState = targetCallState;
- mShouldDisconnectUponFailure = shouldDisconnectUponFailure;
}
@Override
@@ -73,68 +64,23 @@
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));
+ 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();
-
+ mCall.addCallStateListener(mCallStateListenerImpl);
return mTransactionResult;
}
+ @Override
+ public void finishTransaction() {
+ mCall.removeCallStateListener(mCallStateListenerImpl);
+ }
+
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;
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
index 615e4bc..3812d15 100644
--- a/src/com/android/server/telecom/voip/VideoStateTranslation.java
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -20,6 +20,11 @@
import android.telecom.Log;
import android.telecom.VideoProfile;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.AnomalyReporterAdapterImpl;
+
+import java.util.UUID;
+
/**
* This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
* states as apposed to Transactional video states (defined in {@link CallAttributes.CallType}.
@@ -41,15 +46,16 @@
* This should be used when the client application is signaling they are changing the video
* state.
*/
- public static int TransactionalVideoStateToVideoProfileState(int transactionalVideo) {
- if (transactionalVideo == CallAttributes.AUDIO_CALL) {
- Log.i(TAG, "%s --> VideoProfile.STATE_AUDIO_ONLY",
- TransactionalVideoState_toString(transactionalVideo));
+ public static int TransactionalVideoStateToVideoProfileState(int callType) {
+ if (callType == CallAttributes.AUDIO_CALL) {
+ Log.i(TAG, "CallAttributes.AUDIO_CALL --> VideoProfile.STATE_AUDIO_ONLY");
return VideoProfile.STATE_AUDIO_ONLY;
- } else {
- Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
- TransactionalVideoState_toString(transactionalVideo));
+ } else if (callType == CallAttributes.VIDEO_CALL) {
+ Log.i(TAG, "CallAttributes.VIDEO_CALL--> VideoProfile.STATE_BIDIRECTIONAL");
return VideoProfile.STATE_BIDIRECTIONAL;
+ } else {
+ Log.w(TAG, "CallType=[%d] does not have a VideoProfile mapping", callType);
+ return VideoProfile.STATE_AUDIO_ONLY;
}
}
@@ -58,26 +64,36 @@
* This should be used when Telecom is informing the client of a video state change.
*/
public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
- if (videoProfileState == VideoProfile.STATE_AUDIO_ONLY) {
- Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
- VideoProfileState_toString(videoProfileState));
- return CallAttributes.AUDIO_CALL;
- } else {
- Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
- VideoProfileState_toString(videoProfileState));
- return CallAttributes.VIDEO_CALL;
+ switch (videoProfileState) {
+ case VideoProfile.STATE_AUDIO_ONLY -> {
+ Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+ VideoProfileStateToString(videoProfileState));
+ return CallAttributes.AUDIO_CALL;
+ }
+ case VideoProfile.STATE_BIDIRECTIONAL, VideoProfile.STATE_TX_ENABLED,
+ VideoProfile.STATE_RX_ENABLED -> {
+ Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+ VideoProfileStateToString(videoProfileState));
+ return CallAttributes.VIDEO_CALL;
+ }
+ default -> {
+ Log.w(TAG, "VideoProfile=[%d] does not have a CallType mapping", videoProfileState);
+ return CallAttributes.AUDIO_CALL;
+ }
}
}
- private static String TransactionalVideoState_toString(int transactionalVideoState) {
+ public static String TransactionalVideoStateToString(int transactionalVideoState) {
if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
return "CallAttributes.AUDIO_CALL";
- } else {
+ } else if (transactionalVideoState == CallAttributes.VIDEO_CALL) {
return "CallAttributes.VIDEO_CALL";
+ } else {
+ return "CallAttributes.UNKNOWN";
}
}
- private static String VideoProfileState_toString(int videoProfileState) {
+ private static String VideoProfileStateToString(int videoProfileState) {
switch (videoProfileState) {
case VideoProfile.STATE_BIDIRECTIONAL -> {
return "VideoProfile.STATE_BIDIRECTIONAL";
@@ -88,7 +104,12 @@
case VideoProfile.STATE_TX_ENABLED -> {
return "VideoProfile.STATE_TX_ENABLED";
}
+ case VideoProfile.STATE_AUDIO_ONLY -> {
+ return "VideoProfile.STATE_AUDIO_ONLY";
+ }
+ default -> {
+ return "VideoProfile.UNKNOWN";
+ }
}
- return "VideoProfile.STATE_AUDIO_ONLY";
}
}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index 3c91158..a589a6d 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,8 +18,10 @@
import android.os.Handler;
import android.os.HandlerThread;
+import android.telecom.CallException;
import android.telecom.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.flags.Flags;
@@ -34,7 +36,7 @@
public class VoipCallTransaction {
//TODO: add log events
- protected static final long TIMEOUT_LIMIT = 5000L;
+ private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L;
/**
* Tracks stats about a transaction for logging purposes.
@@ -129,96 +131,141 @@
protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
protected final String mTransactionName = this.getClass().getSimpleName();
- private HandlerThread mHandlerThread;
- protected Handler mHandler;
+ private final HandlerThread mHandlerThread;
+ protected final Handler mHandler;
protected TransactionManager.TransactionCompleteListener mCompleteListener;
- protected List<VoipCallTransaction> mSubTransactions;
- protected TelecomSystem.SyncRoot mLock;
+ protected final List<VoipCallTransaction> mSubTransactions;
+ protected final TelecomSystem.SyncRoot mLock;
+ protected final long mTransactionTimeoutMs;
protected final Stats mStats;
public VoipCallTransaction(
- List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
+ List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock,
+ long timeoutMs) {
mSubTransactions = subTransactions;
mHandlerThread = new HandlerThread(this.toString());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mLock = lock;
+ mTransactionTimeoutMs = timeoutMs;
mStats = Flags.enableCallSequencing() ? new Stats() : null;
}
- public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
- this(null /** mSubTransactions */, lock);
+ public VoipCallTransaction(List<VoipCallTransaction> subTransactions,
+ TelecomSystem.SyncRoot lock) {
+ this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS);
+ }
+ public VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
+ this(null /* mSubTransactions */, lock, timeoutMs);
}
- public void start() {
+ public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
+ this(null /* mSubTransactions */, lock);
+ }
+
+ public final void start() {
if (mStats != null) mStats.markStarted();
// post timeout work
CompletableFuture<Void> future = new CompletableFuture<>();
- mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+ mHandler.postDelayed(() -> future.complete(null), mTransactionTimeoutMs);
future.thenApplyAsync((x) -> {
- if (mCompleted.getAndSet(true)) {
- return null;
- }
- if (mCompleteListener != null) {
- mCompleteListener.onTransactionTimeout(mTransactionName);
- }
timeout();
return null;
}, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ ".s", mLock));
+ processTransactions();
+ }
+
+ /**
+ * By default, this processes this transaction. For VoipCallTransactions with sub-transactions,
+ * this implementation should be overwritten to handle also processing sub-transactions.
+ */
+ protected void processTransactions() {
scheduleTransaction();
}
- protected void scheduleTransaction() {
+ /**
+ * This method is called when the transaction has finished either successfully or exceptionally.
+ * VoipCallTransactions that are extending this class should override this method to clean up
+ * any leftover state.
+ */
+ protected void finishTransaction() {
+
+ }
+
+ protected final void scheduleTransaction() {
LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode() + ".pT", mLock);
+ mTransactionName + "@" + hashCode() + ".sT", mLock);
CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
future.thenComposeAsync(this::processTransaction, executor)
.thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
- mCompleted.set(true);
- if (mCompleteListener != null) {
- mCompleteListener.onTransactionCompleted(result, mTransactionName);
- }
- finish(result);
+ notifyListenersOfResult(result);
return null;
- }, executor)
- .exceptionallyAsync((throwable -> {
+ }, executor)
+ .exceptionally((throwable -> {
+ // Do NOT wait for the timeout in order to finish this failed transaction.
+ // Instead, propagate the failure to the other transactions immediately!
+ String errorMessage = throwable != null ? throwable.getMessage() :
+ "encountered an exception while processing " + mTransactionName;
+ notifyListenersOfResult(new VoipCallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN, errorMessage));
Log.e(this, throwable, "Error while executing transaction.");
return null;
- }), executor);
+ }));
}
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ protected void notifyListenersOfResult(VoipCallTransactionResult result){
+ mCompleted.set(true);
+ finish(result);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionCompleted(result, mTransactionName);
+ }
+ }
+
+ protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
return CompletableFuture.completedFuture(
new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null));
}
- public void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
+ public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
mCompleteListener = listener;
}
- public void timeout() {
+ @VisibleForTesting
+ public final void timeout() {
+ if (mCompleted.getAndSet(true)) {
+ return;
+ }
finish(true, null);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionTimeout(mTransactionName);
+ }
}
- public void finish(VoipCallTransactionResult result) {
+ @VisibleForTesting
+ public final Handler getHandler() {
+ return mHandler;
+ }
+
+ public final void finish(VoipCallTransactionResult result) {
finish(false, result);
}
- public void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+ private void finish(boolean isTimedOut, VoipCallTransactionResult result) {
if (mStats != null) mStats.markComplete(isTimedOut, result);
+ finishTransaction();
// finish all sub transactions
if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
}
- mHandlerThread.quit();
+ mHandlerThread.quitSafely();
}
/**
* @return Stats related to this transaction if stats are enabled, null otherwise.
*/
- public Stats getStats() {
+ public final Stats getStats() {
return mStats;
}
}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
index ffc0255..50871f2 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
@@ -22,7 +22,10 @@
public class VoipCallTransactionResult {
public static final int RESULT_SUCCEED = 0;
- public static final int RESULT_FAILED = 1;
+
+ // NOTE: if the VoipCallTransactionResult should not use the RESULT_SUCCEED to represent a
+ // successful transaction, use an error code defined in the
+ // {@link android.telecom.CallException} class
private final int mResult;
private final String mMessage;
diff --git a/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml
index 5500444..00e028e 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 línia"</string>
+ <string name="start_stream" msgid="3567634786280097431">"inicia l\'estríming"</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/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 4bca30d..7646c2d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -1036,6 +1036,7 @@
call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
assert(call.isVideoCallingSupportedByPhoneAccount());
assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ call.setIsCreateConnectionComplete(true);
}
/**
@@ -1059,6 +1060,7 @@
call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle());
assert(!call.isVideoCallingSupportedByPhoneAccount());
assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+ call.setIsCreateConnectionComplete(true);
}
/**
diff --git a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
index 696867e..57aee62 100644
--- a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
+++ b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
@@ -16,10 +16,13 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.NotificationManager;
@@ -27,6 +30,7 @@
import androidx.test.filters.SmallTest;
+import com.android.server.telecom.settings.BlockedNumbersActivity;
import com.android.server.telecom.settings.BlockedNumbersUtil;
import org.junit.Before;
@@ -58,4 +62,16 @@
NotificationManager mgr = mComponentContextFixture.getNotificationManager();
verify(mgr).cancelAsUser(isNull(), anyInt(), any(UserHandle.class));
}
+
+ /**
+ * Verify that when Telephony isn't present we can still check if a number is an emergency
+ * number in the {@link BlockedNumbersActivity} and not crash.
+ */
+ @SmallTest
+ @Test
+ public void testBlockedNumbersActivityEmergencyCheckWithNoTelephony() {
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertFalse(BlockedNumbersActivity.isEmergencyNumber(mContext, "911"));
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index c516c8e..d5e903b 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -60,9 +60,11 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import static org.mockito.Mockito.reset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.Executor;
@RunWith(JUnit4.class)
public class BluetoothDeviceManagerTest extends TelecomTestCase {
@@ -75,6 +77,7 @@
@Mock BluetoothLeAudio mBluetoothLeAudio;
@Mock AudioManager mockAudioManager;
@Mock AudioDeviceInfo mSpeakerInfo;
+ @Mock Executor mExecutor;
BluetoothDeviceManager mBluetoothDeviceManager;
BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -114,6 +117,7 @@
mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
mockAudioManager = mContext.getSystemService(AudioManager.class);
+ mExecutor = mContext.getMainExecutor();
ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
@@ -750,6 +754,31 @@
assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled());
}
+ @SmallTest
+ @Test
+ public void testRegisterLeAudioCallbackNoPostpone() {
+ reset(mBluetoothLeAudio);
+ when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(false);
+ serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+ (BluetoothProfile) mBluetoothLeAudio);
+ // Second time on purpose
+ serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+ (BluetoothProfile) mBluetoothLeAudio);
+ verify(mExecutor, times(0)).execute(any());
+ verify(mBluetoothLeAudio, times(1)).registerCallback(any(Executor.class),
+ any(BluetoothLeAudio.Callback.class));
+ }
+
+ @SmallTest
+ @Test
+ public void testRegisterLeAudioCallbackWithPostpone() {
+ reset(mBluetoothLeAudio);
+ when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(true);
+ serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+ (BluetoothProfile) mBluetoothLeAudio);
+ verify(mExecutor, times(1)).execute(any());
+ }
+
private void assertClearHearingAidOrLeCommunicationDevice(
boolean flagEnabled, int device_type
) {
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 07dd350..1c885c1 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -21,6 +21,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -249,7 +250,18 @@
@SmallTest
@Test
- public void testConnectBtWithoutAddress() {
+ public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() {
+ when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true);
+ verifyConnectBtWithoutAddress();
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() {
+ verifyConnectBtWithoutAddress();
+ }
+
+ private void verifyConnectBtWithoutAddress() {
when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true);
BluetoothRouteManager sm = setupStateMachine(
BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
@@ -266,7 +278,15 @@
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
- verifyConnectionAttempt(DEVICE1, 1);
+ // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio)
+ // as the device is already "connected" as per how the state machine was initialized.
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ verify(mDeviceManager, never()).disconnectAudio();
+ } else {
+ // Legacy behavior
+ verifyConnectionAttempt(DEVICE1, 1);
+ verify(mDeviceManager, times(1)).disconnectAudio();
+ }
assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+ ":" + DEVICE1.getAddress(),
sm.getCurrentState().getName());
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 97405a3..1d641ba 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.tests.TelecomSystemTest.TEST_TIMEOUT;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -34,6 +36,8 @@
import static org.mockito.Mockito.when;
import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
import android.telecom.DisconnectCause;
import android.util.SparseArray;
@@ -67,6 +71,7 @@
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@RunWith(JUnit4.class)
@@ -423,9 +428,12 @@
Call call = mock(Call.class);
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
when(call.getState()).thenReturn(CallState.RINGING);
+ handleWaitForBtIcsBinding(call);
// Make sure appropriate messages are sent when we add a RINGING call
mCallAudioManager.onCallAdded(call);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
assertEquals(call, mCallAudioManager.getForegroundCall());
verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
@@ -556,10 +564,14 @@
Call call = createAudioProcessingCall();
+
when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+ handleWaitForBtIcsBinding(call);
mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
CallState.SIMULATED_RINGING);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
.setHasActiveOrDialingCalls(false)
@@ -810,9 +822,12 @@
private Call createSimulatedRingingCall() {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+ handleWaitForBtIcsBinding(call);
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
mCallAudioManager.onCallAdded(call);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
assertEquals(call, mCallAudioManager.getForegroundCall());
@@ -838,8 +853,11 @@
private Call createIncomingCall() {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.RINGING);
+ handleWaitForBtIcsBinding(call);
mCallAudioManager.onCallAdded(call);
+ mCallAudioManager.getCallRingingFuture().join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
assertEquals(call, mCallAudioManager.getForegroundCall());
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
@@ -924,4 +942,10 @@
assertEquals(expected.isTonePlaying, actual.isTonePlaying);
assertEquals(expected.foregroundCallIsVoip, actual.foregroundCallIsVoip);
}
+
+ private void handleWaitForBtIcsBinding(Call call) {
+ when(mFlags.separatelyBindToBtIncallService()).thenReturn(true);
+ CompletableFuture<Boolean> btBindingFuture = CompletableFuture.completedFuture(true);
+ when(call.getBtIcsFuture()).thenReturn(btBindingFuture);
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 0a53eb0..59473bd 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -20,7 +20,6 @@
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
-import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
@@ -35,7 +34,7 @@
import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
-import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
@@ -48,30 +47,37 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
import android.os.UserHandle;
import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteController;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import org.junit.After;
@@ -82,6 +88,7 @@
import org.mockito.Mock;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
@RunWith(JUnit4.class)
@@ -94,6 +101,14 @@
@Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
@Mock IAudioService mAudioService;
@Mock BluetoothRouteManager mBluetoothRouteManager;
+ @Mock BluetoothDeviceManager mBluetoothDeviceManager;
+ @Mock BluetoothAdapter mBluetoothAdapter;
+ @Mock StatusBarNotifier mockStatusBarNotifier;
+ @Mock AudioDeviceInfo mAudioDeviceInfo;
+ @Mock BluetoothLeAudio mBluetoothLeAudio;
+ @Mock CallAudioManager mCallAudioManager;
+ @Mock Call mCall;
+ @Mock private TelecomSystem.SyncRoot mLock;
private AudioRoute mEarpieceRoute;
private AudioRoute mSpeakerRoute;
private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -109,7 +124,7 @@
@Override
public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
AudioManager audioManager) {
- return new AudioRoute(type, bluetoothAddress, null);
+ return new AudioRoute(type, bluetoothAddress, mAudioDeviceInfo);
}
};
@@ -124,18 +139,39 @@
});
when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
.thenReturn(null);
+ when(mAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(List.of(mAudioDeviceInfo));
+ when(mAudioManager.getCommunicationDevice()).thenReturn(mAudioDeviceInfo);
+ when(mAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+ .thenReturn(true);
when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
when(mContext.getAttributionTag()).thenReturn("");
doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
any(CallAudioState.class));
when(mCallsManager.getCurrentUserHandle()).thenReturn(
new UserHandle(UserHandle.USER_SYSTEM));
+ when(mCallsManager.getLock()).thenReturn(mLock);
+ when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
+ when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+ .thenReturn(true);
+ when(mBluetoothDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+ when(mBluetoothAdapter.getActiveDevices(anyInt())).thenReturn(List.of(BLUETOOTH_DEVICE_1));
+ when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio);
+ when(mBluetoothLeAudio.getGroupId(any(BluetoothDevice.class))).thenReturn(1);
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ when(mAudioDeviceInfo.getAddress()).thenReturn(BT_ADDRESS_1);
mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
- mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
+ mAudioRouteFactory, mWiredHeadsetManager,
+ mBluetoothRouteManager, mockStatusBarNotifier, mFeatureFlags);
mController.setAudioRouteFactory(mAudioRouteFactory);
mController.setAudioManager(mAudioManager);
mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+ mController.setCallAudioManager(mCallAudioManager);
+ when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
}
@After
@@ -166,36 +202,27 @@
@SmallTest
@Test
- public void testActivateAndRemoveBluetoothDeviceDuringCall() {
- doAnswer(invocation -> {
- mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
- return true;
- }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
-
+ public void testInitializeWithWiredHeadset() {
+ AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+ when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
mController.initialize();
- mController.setActive(true);
- mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
- BLUETOOTH_DEVICE_1);
+ assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+ assertEquals(2, mController.getAvailableRoutes().size());
+ assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+ }
+
+ @SmallTest
+ @Test
+ public void testNormalCallRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is set to speaker. This will trigger pending
+ // message to wait for SPEAKER_ON message once communication device is set before routing.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+
CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
- CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
- | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
- verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
- any(CallAudioState.class), eq(expectedState));
-
- mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
- AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
- verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
- nullable(AudioDeviceInfo.class));
-
- expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
- CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
- | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
- verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
- any(CallAudioState.class), eq(expectedState));
-
- mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
- BLUETOOTH_DEVICE_1);
- expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
new HashSet<>());
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
@@ -204,6 +231,49 @@
@SmallTest
@Test
+ public void testVideoCallHoldRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is not defaulted to speaker when a video call
+ // is not the foreground call.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+ }
+
+ @SmallTest
+ @Test
+ public void testVideoCallRouteToSpeaker() {
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+ // Verify that pending audio destination route is set to speaker. This will trigger pending
+ // message to wait for SPEAKER_ON message once communication device is set before routing.
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_SPEAKER, pendingRoute.getDestRoute().getType());
+
+ // Mock SPEAKER_ON message received by controller.
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Verify that audio is routed to wired headset if it's present.
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
public void testActiveDeactivateBluetoothDevice() {
mController.initialize();
mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -229,14 +299,6 @@
@SmallTest
@Test
public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
- doAnswer(invocation -> {
- mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
- return true;
- }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
- doAnswer(invocation -> {
- mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
- return true;
- }).when(mAudioManager).clearCommunicationDevice();
when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
mController.initialize();
@@ -253,15 +315,15 @@
assertFalse(mController.isActive());
mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
- verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
- nullable(AudioDeviceInfo.class));
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
assertTrue(mController.isActive());
mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
assertTrue(mController.isActive());
mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
- verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
assertFalse(mController.isActive());
}
@@ -456,8 +518,7 @@
@SmallTest
@Test
public void testToggleMute() throws Exception {
- when(mAudioManager.isMasterMute()).thenReturn(false);
-
+ when(mAudioManager.isMicrophoneMute()).thenReturn(false);
mController.initialize();
mController.setActive(true);
@@ -470,7 +531,7 @@
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
any(CallAudioState.class), eq(expectedState));
- when(mAudioManager.isMasterMute()).thenReturn(true);
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
mController.sendMessageWithSessionInfo(MUTE_OFF);
expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
@@ -480,4 +541,236 @@
verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
any(CallAudioState.class), eq(expectedState));
}
+
+ @SmallTest
+ @Test
+ public void testMuteOffAfterCallEnds() throws Exception {
+ when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+ mController.initialize();
+ mController.setActive(true);
+
+ mController.sendMessageWithSessionInfo(MUTE_ON);
+ CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch to NO_FOCUS to indicate call termination and verify mute is reset.
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+ anyInt(), anyString());
+ verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testIgnoreAutoRouteToWatch() {
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+ when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Connect wired headset.
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Disconnect wired headset and ensure Telecom routes to earpiece instead of the BT route.
+ mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, null , BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectDisconnectScoDuringCall() {
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectLeDeviceDuringCall() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectHearingAidDuringCall() {
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+ verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchBetweenLeAndScoDevices() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ BluetoothDevice scoDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+ BLUETOOTH_DEVICES.add(scoDevice);
+
+ // Add SCO device.
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch to SCO and verify active device is updated.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0, scoDevice.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, scoDevice);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Disconnect SCO and verify audio routed back to LE audio.
+ BLUETOOTH_DEVICES.remove(scoDevice);
+ mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testFallbackWhenBluetoothConnectionFails() {
+ when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+ .thenReturn(false);
+
+ AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+ when(mAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+ BluetoothDevice scoDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+ BLUETOOTH_DEVICES.add(scoDevice);
+
+ // Add SCO device.
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch to SCO but reject connection and make sure audio is routed back to LE device.
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Cleanup supported devices for next test
+ BLUETOOTH_DEVICES.remove(scoDevice);
+ }
+
+ @SmallTest
+ @Test
+ public void testIgnoreLeRouteWhenServiceUnavailable() {
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+ .thenReturn(BLUETOOTH_DEVICE_1);
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+
+ when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(null);
+ // Switch baseline to verify that we don't route back to LE audio this time.
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 0, (String) null);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ private void verifyConnectBluetoothDevice(int audioType) {
+ mController.initialize();
+ mController.setActive(true);
+
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, BLUETOOTH_DEVICE_1);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, audioType, BT_ADDRESS_1);
+ if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+ 0, BLUETOOTH_DEVICE_1);
+ } else {
+ verify(mAudioManager, timeout(TEST_TIMEOUT))
+ .setCommunicationDevice(nullable(AudioDeviceInfo.class));
+ }
+
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Test hearing aid pair and ensure second device isn't added as a route
+ if (audioType == AudioRoute.TYPE_BLUETOOTH_HA) {
+ BluetoothDevice hearingAidDevice2 =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, hearingAidDevice2);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ // Verify that supported BT devices only shows the first connected hearing aid device.
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+ }
+
+ private void verifyDisconnectBluetoothDevice(int audioType) {
+ mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, audioType, BLUETOOTH_DEVICE_1);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+ } else {
+ verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+ }
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index d2da505..e97de2e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -1230,8 +1231,9 @@
@SmallTest
@Test
- public void testQuiescentBluetoothRouteResetMute() {
+ public void testQuiescentBluetoothRouteResetMute() throws Exception {
when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true);
+ when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
mContext,
mockCallsManager,
@@ -1264,6 +1266,7 @@
CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
| CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ when(mockAudioManager.isMicrophoneMute()).thenReturn(true);
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.NO_FOCUS);
@@ -1272,9 +1275,8 @@
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());
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ verify(mockAudioService).setMicrophoneMute(eq(false), anyString(), anyInt(), eq(null));
}
@SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
index 9101a19..b8b9560 100644
--- a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -40,6 +40,7 @@
import com.android.server.telecom.CallEndpointController;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.flags.FeatureFlags;
import org.junit.Before;
import org.junit.After;
@@ -101,7 +102,10 @@
@Before
public void setUp() throws Exception {
super.setUp();
- mCallEndpointController = new CallEndpointController(mMockContext, mCallsManager);
+ mCallEndpointController = new CallEndpointController(
+ mMockContext,
+ mCallsManager,
+ mFeatureFlags);
doReturn(new HashSet<>(Arrays.asList(mCall))).when(mCallsManager).getTrackedCalls();
doReturn(mConnectionService).when(mCall).getConnectionService();
doReturn(mCallAudioManager).when(mCallsManager).getCallAudioManager();
diff --git a/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
new file mode 100644
index 0000000..6deade4
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.app.IntentForwarderActivity;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIntentProcessor;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelephonyUtil;
+
+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.MockitoSession;
+
+import java.util.concurrent.CompletableFuture;
+
+/** Unit tests for CollIntentProcessor class. */
+@RunWith(JUnit4.class)
+public class CallIntentProcessorTest extends TelecomTestCase {
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Mock
+ private CallsManager mCallsManager;
+ @Mock
+ private DefaultDialerCache mDefaultDialerCache;
+ @Mock
+ private Context mMockCreateContextAsUser;
+ @Mock
+ private UserManager mMockCurrentUserManager;
+ @Mock
+ private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private ResolveInfo mResolveInfo;
+ @Mock
+ private ComponentName mComponentName;
+ @Mock
+ private ComponentInfo mComponentInfo;
+ @Mock
+ private CompletableFuture<Call> mCall;
+ private CallIntentProcessor mCallIntentProcessor;
+ private static final UserHandle PRIVATE_SPACE_USERHANDLE = new UserHandle(12);
+ private static final String TEST_PACKAGE_NAME = "testPackageName";
+ private static final Uri TEST_PHONE_NUMBER = Uri.parse("tel:1234567890");
+ private static final Uri TEST_EMERGENCY_PHONE_NUMBER = Uri.parse("tel:911");
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ when(mContext.createContextAsUser(any(UserHandle.class), eq(0))).thenReturn(
+ mMockCreateContextAsUser);
+ when(mMockCreateContextAsUser.getSystemService(UserManager.class)).thenReturn(
+ mMockCurrentUserManager);
+ mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager, mDefaultDialerCache,
+ mFeatureFlags);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+ when(mCallsManager.getPhoneNumberUtilsAdapter()).thenReturn(mPhoneNumberUtilsAdapter);
+ when(mPhoneNumberUtilsAdapter.isUriNumber(anyString())).thenReturn(true);
+ when(mCallsManager.startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+ any(UserHandle.class), any(Intent.class), anyString())).thenReturn(mCall);
+ when(mCall.thenAccept(any())).thenReturn(null);
+ }
+
+ @Test
+ public void testNonPrivateSpaceCall_noConsentDialogShown() {
+ setPrivateSpaceFlagsEnabled();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, UserHandle.CURRENT);
+ when(mCallsManager.isSelfManaged(any(), eq(UserHandle.CURRENT))).thenReturn(false);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ verify(mContext, never()).startActivityAsUser(any(Intent.class), any(UserHandle.class));
+
+ // Verify that the call proceeds as normal since the dialog was not shown
+ verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+ eq(UserHandle.CURRENT), eq(intent), eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testPrivateSpaceCall_isSelfManaged_noDialogShown() {
+ setPrivateSpaceFlagsEnabled();
+ markInitiatingUserAsPrivateProfile();
+ resolveAsIntentForwarderActivity();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+ when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(true);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ verify(mContext, never()).startActivityAsUser(any(Intent.class),
+ eq(PRIVATE_SPACE_USERHANDLE));
+
+ // Verify that the call proceeds as normal since the dialog was not shown
+ verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+ eq(PRIVATE_SPACE_USERHANDLE), eq(intent), eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testPrivateSpaceCall_isEmergency_noDialogShown() {
+ MockitoSession session = ExtendedMockito.mockitoSession().mockStatic(
+ TelephonyUtil.class).startMocking();
+ ExtendedMockito.doReturn(true).when(
+ () -> TelephonyUtil.shouldProcessAsEmergency(any(), any()));
+
+ setPrivateSpaceFlagsEnabled();
+ markInitiatingUserAsPrivateProfile();
+ resolveAsIntentForwarderActivity();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_EMERGENCY_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+ when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ verify(mContext, never()).startActivityAsUser(any(Intent.class),
+ eq(PRIVATE_SPACE_USERHANDLE));
+ session.finishMocking();
+ }
+
+ @Test
+ public void testPrivateSpaceCall_showConsentDialog() {
+ setPrivateSpaceFlagsEnabled();
+ markInitiatingUserAsPrivateProfile();
+ resolveAsIntentForwarderActivity();
+
+ Intent intent = new Intent(Intent.ACTION_CALL);
+ intent.setData(TEST_PHONE_NUMBER);
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+ when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+ mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+ // Consent dialog should be shown
+ verify(mContext).startActivityAsUser(any(Intent.class), eq(PRIVATE_SPACE_USERHANDLE));
+
+ /// Verify that the call does not proceeds as normal since the dialog was shown
+ verify(mCallsManager, never()).startOutgoingCall(any(), any(), any(), any(), any(),
+ anyString());
+ }
+
+ private void setPrivateSpaceFlagsEnabled() {
+ mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+ android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION);
+ }
+
+ private void markInitiatingUserAsPrivateProfile() {
+ when(mMockCurrentUserManager.isPrivateProfile()).thenReturn(true);
+ }
+
+ private void resolveAsIntentForwarderActivity() {
+ when(mComponentName.getShortClassName()).thenReturn(
+ IntentForwarderActivity.FORWARD_INTENT_TO_PARENT);
+ when(mComponentInfo.getComponentName()).thenReturn(mComponentName);
+ when(mResolveInfo.getComponentInfo()).thenReturn(mComponentInfo);
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class),
+ any(PackageManager.ResolveInfoFlags.class),
+ eq(PRIVATE_SPACE_USERHANDLE.getIdentifier()))).thenReturn(mResolveInfo);
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index fa35f25..cb04dc3 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -24,6 +24,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
@@ -122,6 +123,7 @@
private static final int CURRENT_USER_ID = 0;
private static final int OTHER_USER_ID = 10;
private static final int MANAGED_USER_ID = 11;
+ private static final int PRIVATE_USER_ID = 12;
private static final String TEST_ISO = "KR";
private static final String TEST_ISO_2 = "JP";
@@ -175,9 +177,22 @@
UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
UserInfo userInfo = new UserInfo(CURRENT_USER_ID, "test", 0);
+ userInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+ userInfo.userType = UserManager.USER_TYPE_FULL_SYSTEM;
+
UserInfo otherUserInfo = new UserInfo(OTHER_USER_ID, "test2", 0);
+ otherUserInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+ otherUserInfo.userType = UserManager.USER_TYPE_FULL_SECONDARY;
+
UserInfo managedProfileUserInfo = new UserInfo(MANAGED_USER_ID, "test3",
- UserInfo.FLAG_MANAGED_PROFILE);
+ UserInfo.FLAG_MANAGED_PROFILE | userInfo.FLAG_PROFILE);
+ managedProfileUserInfo.profileGroupId = 90210;
+ managedProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
+
+ UserInfo privateProfileUserInfo = new UserInfo(PRIVATE_USER_ID, "private",
+ UserInfo.FLAG_PROFILE);
+ privateProfileUserInfo.profileGroupId = 90210;
+ privateProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_PRIVATE;
doAnswer(new Answer<Uri>() {
@Override
@@ -188,16 +203,44 @@
when(userManager.isUserRunning(any(UserHandle.class))).thenReturn(true);
when(userManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true);
- when(userManager.hasUserRestriction(any(String.class), any(UserHandle.class)))
+ when(userManager.hasUserRestrictionForUser(any(String.class), any(UserHandle.class)))
.thenReturn(false);
when(userManager.getAliveUsers())
.thenReturn(Arrays.asList(userInfo, otherUserInfo, managedProfileUserInfo));
+ configureContextForUser(CURRENT_USER_ID, userInfo);
when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo);
+
+ configureContextForUser(OTHER_USER_ID, otherUserInfo);
when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo);
+
+ configureContextForUser(MANAGED_USER_ID, managedProfileUserInfo);
when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo);
+
+ configureContextForUser(PRIVATE_USER_ID, privateProfileUserInfo);
+ when(userManager.getUserInfo(eq(PRIVATE_USER_ID))).thenReturn(privateProfileUserInfo);
+
PackageManager packageManager = mContext.getPackageManager();
when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(false);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+ }
+
+ /**
+ * Yuck; this is absolutely wretched that we have to mock things out in this way.
+ * Because the preferred way to get info about a user is to first user
+ * {@link Context#createContextAsUser(UserHandle, int)} to first get a user-specific context,
+ * and to then query the {@link UserManager} instance to see if it's a profile, we need to do
+ * all of this really gross mocking.
+ * @param userId The userid.
+ * @param info The associated userinfo.
+ */
+ private void configureContextForUser(int userId, UserInfo info) {
+ Context mockContext = mock(Context.class);
+ mComponentContextFixture.addContextForUser(UserHandle.of(userId), mockContext);
+ UserManager mockUserManager = mock(UserManager.class);
+ when(mockUserManager.getUserInfo(eq(userId))).thenReturn(info);
+ when(mockUserManager.isProfile()).thenReturn(info.isProfile());
+ when(mockContext.getSystemService(eq(UserManager.class))).thenReturn(mockUserManager);
}
@Override
@@ -232,7 +275,7 @@
@Test
public void testDontLogChoosingAccountCall() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -335,7 +378,7 @@
@Test
public void testLogCallDirectionOutgoing() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeOutgoingCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -360,7 +403,7 @@
@Test
public void testLogCallDirectionIncoming() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeIncomingCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -386,7 +429,7 @@
@Test
public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOff() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeMissedCall = makeFakeCall(
DisconnectCause.MISSED, // disconnectCauseCode
false, // isConference
@@ -417,7 +460,7 @@
@Test
public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOn() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeMissedCall = makeFakeCall(
DisconnectCause.MISSED, // disconnectCauseCode
false, // isConference
@@ -448,7 +491,7 @@
@Test
public void testLogCallDirectionRejected() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeMissedCall = makeFakeCall(
DisconnectCause.REJECTED, // disconnectCauseCode
false, // isConference
@@ -474,7 +517,7 @@
@Test
public void testCreationTimeAndAge() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
long currentTime = System.currentTimeMillis();
long duration = 1000L;
Call fakeCall = makeFakeCall(
@@ -502,7 +545,7 @@
@Test
public void testLogPhoneAccountId() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -526,7 +569,7 @@
@Test
public void testLogCorrectPhoneNumber() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -553,7 +596,7 @@
@Test
public void testLogCallVideoFeatures() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeVideoCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -611,7 +654,8 @@
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
assertFalse(uris.getAllValues().contains(
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+ assertFalse(uris.getAllValues().contains(
+ ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
for (ContentValues v : values.getAllValues()) {
assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -656,7 +700,8 @@
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
assertFalse(uris.getAllValues().contains(
ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+ assertFalse(uris.getAllValues().contains(
+ ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
for (ContentValues v : values.getAllValues()) {
assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
@@ -666,6 +711,8 @@
@MediumTest
@Test
public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromManagedProfile() {
+ UserManager userManager = mContext.getSystemService(UserManager.class);
+ when(userManager.isManagedProfile()).thenReturn(true);
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
.thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle,
PhoneAccount.CAPABILITY_MULTI_USER));
@@ -694,6 +741,72 @@
}
@MediumTest
+ @Test
+ public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfile() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ UserHandle.of(PRIVATE_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call placed through private space should only show up in the private space
+ // call logs.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(MANAGED_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
+ @Test
+ public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfileNoRefactor() {
+ // Same as the above test, but turns off the hidden deps refactor; there are some minor
+ // differences in how we detect profiles, so we want to ensure this works both ways.
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ UserHandle.of(PRIVATE_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call placed through work dialer should be inserted to managed profile only.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(MANAGED_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
@FlakyTest
@Test
public void testLogCallDirectionOutgoingFromManagedProfile() {
@@ -719,6 +832,7 @@
// profile only.
verifyNoInsertionInUser(CURRENT_USER_ID);
verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(PRIVATE_USER_ID);
ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -749,6 +863,7 @@
// profile only.
verifyNoInsertionInUser(CURRENT_USER_ID);
verifyNoInsertionInUser(OTHER_USER_ID);
+ verifyNoInsertionInUser(PRIVATE_USER_ID);
ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
Integer.valueOf(Calls.INCOMING_TYPE));
@@ -761,7 +876,7 @@
@Test
public void testLogCallDataUsageSet() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeVideoCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -788,7 +903,7 @@
@Test
public void testLogCallDataUsageNotSet() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
Call fakeVideoCall = makeFakeCall(
DisconnectCause.OTHER, // disconnectCauseCode
false, // isConference
@@ -842,7 +957,7 @@
@Test
public void testLogCallWhenExternalCallOnWatch() {
when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
- .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
PackageManager packageManager = mContext.getPackageManager();
when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 8210686..241216a 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -378,4 +378,22 @@
verify(mContext, times(1)).
unbindService(any(ServiceConnection.class));
}
+
+ /**
+ * Verifies that calling formatNumberToE164 will not crash when Telephony is not present and
+ * we can't ascertain the network country ISO.
+ */
+ @Test
+ public void testFormatNumberToE164WhenNoTelephony() {
+ // Need to do this even though we're just testing the helper
+ startProcessWithNoGateWayInfo();
+
+ CallRedirectionProcessorHelper helper = new CallRedirectionProcessorHelper(mContext,
+ mCallsManager, mPhoneAccountRegistrar);
+ when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertEquals(Uri.fromParts("tel", "6505551212", null),
+ helper.formatNumberToE164(
+ Uri.fromParts("tel", "6505551212", null)));
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index e06938d..a22d2ca 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -22,24 +22,29 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
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.CallEndpoint;
import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -56,6 +61,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.server.telecom.CachedAvailableEndpointsChange;
+import com.android.server.telecom.CachedCurrentEndpointChange;
+import com.android.server.telecom.CachedMuteStateChange;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallIdMapper;
import com.android.server.telecom.CallState;
@@ -63,6 +71,7 @@
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ClockProxy;
import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.EmergencyCallHelper;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
@@ -78,6 +87,7 @@
import org.mockito.Mockito;
import java.util.Collections;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
public class CallTest extends TelecomTestCase {
@@ -100,7 +110,6 @@
@Mock private PhoneAccountRegistrar mMockPhoneAccountRegistrar;
@Mock private ClockProxy mMockClockProxy;
@Mock private ToastFactory mMockToastProxy;
- @Mock private Toast mMockToast;
@Mock private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
@Mock private ConnectionServiceWrapper mMockConnectionService;
@Mock private TransactionalServiceWrapper mMockTransactionalService;
@@ -117,8 +126,14 @@
eq(SIM_1_HANDLE));
doReturn(new ComponentName(mContext, CallTest.class))
.when(mMockConnectionService).getComponentName();
- doReturn(mMockToast).when(mMockToastProxy).makeText(any(), anyInt(), anyInt());
doReturn(UserHandle.CURRENT).when(mMockCallsManager).getCurrentUserHandle();
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+ .thenReturn(false);
+ when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+ .thenReturn(false);
+ EmergencyCallHelper helper = mock(EmergencyCallHelper.class);
+ doReturn(helper).when(mMockCallsManager).getEmergencyCallHelper();
}
@After
@@ -138,6 +153,211 @@
}
/**
+ * Verify Call#setVideoState will only upgrade to video if the PhoneAccount supports video
+ * state capabilities
+ */
+ @Test
+ @SmallTest
+ public void testSetVideoStateForTransactionalCalls() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ call.setIsTransactionalCall(true);
+ call.setTransactionServiceWrapper(tsw);
+ assertTrue(call.isTransactionalCall());
+ assertNotNull(call.getTransactionServiceWrapper());
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+ // VoIP apps using transactional APIs must register a PhoneAccount that supports
+ // video calling capabilities or the video state will be defaulted to audio
+ assertFalse(call.isVideoCallingSupportedByPhoneAccount());
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+
+ call.setVideoCallingSupportedByPhoneAccount(true);
+ assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+
+ // After the PhoneAccount signals it supports video calling, video state changes can occur
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+ }
+
+ /**
+ * Verify all video state changes are echoed out to the TransactionalServiceWrapper
+ */
+ @Test
+ @SmallTest
+ public void testToggleTransactionalVideoState() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ call.setIsTransactionalCall(true);
+ call.setTransactionServiceWrapper(tsw);
+ call.setVideoCallingSupportedByPhoneAccount(true);
+ assertTrue(call.isTransactionalCall());
+ assertNotNull(call.getTransactionServiceWrapper());
+ assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(2)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+ call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+ verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.AUDIO_CALL);
+
+ call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+ verify(tsw, times(3)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+ }
+
+ @Test
+ public void testMultipleCachedMuteStateChanges() {
+ when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+ assertNull(call.getTransactionServiceWrapper());
+
+ call.cacheServiceCallback(new CachedMuteStateChange(true));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+
+ call.cacheServiceCallback(new CachedMuteStateChange(false));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+
+ CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+ .getCachedServiceCallbacks()
+ .get(CachedMuteStateChange.ID);
+
+ assertFalse(currentCacheMuteState.isMuted());
+
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).onMuteStateChanged(any(), eq(false));
+ assertEquals(0, call.getCachedServiceCallbacks().size());
+ }
+
+ @Test
+ public void testMultipleCachedCurrentEndpointChanges() {
+ when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+ CallEndpoint speaker = Mockito.mock(CallEndpoint.class);
+ when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+ when(speaker.getEndpointType()).thenReturn(CallEndpoint.TYPE_SPEAKER);
+
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+ assertNull(call.getTransactionServiceWrapper());
+
+ call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+
+ call.cacheServiceCallback(new CachedCurrentEndpointChange(speaker));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+
+ CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+ .getCachedServiceCallbacks()
+ .get(CachedCurrentEndpointChange.ID);
+
+ assertEquals(CallEndpoint.TYPE_SPEAKER,
+ currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+ assertEquals(0, call.getCachedServiceCallbacks().size());
+ }
+
+ @Test
+ public void testMultipleCachedAvailableEndpointChanges() {
+ when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+ CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
+ Set<CallEndpoint> initialSet = Set.of(earpiece);
+ Set<CallEndpoint> finalSet = Set.of(earpiece, bluetooth);
+ when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+ when(bluetooth.getEndpointType()).thenReturn(CallEndpoint.TYPE_BLUETOOTH);
+
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+ assertNull(call.getTransactionServiceWrapper());
+
+ call.cacheServiceCallback(new CachedAvailableEndpointsChange(initialSet));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+
+ call.cacheServiceCallback(new CachedAvailableEndpointsChange(finalSet));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+
+ CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+ .getCachedServiceCallbacks()
+ .get(CachedAvailableEndpointsChange.ID);
+
+ assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+ assertEquals(0, call.getCachedServiceCallbacks().size());
+ }
+
+ /**
+ * verify that if multiple types of cached callbacks are added to the call, the call executes
+ * all the callbacks once the service is set.
+ */
+ @Test
+ public void testAllCachedCallbacks() {
+ when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+ CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
+ Set<CallEndpoint> availableEndpointsSet = Set.of(earpiece, bluetooth);
+ when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+ when(bluetooth.getEndpointType()).thenReturn(CallEndpoint.TYPE_BLUETOOTH);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+ // The call should have a null service so that callbacks are cached
+ assertNull(call.getTransactionServiceWrapper());
+
+ // add cached callbacks
+ call.cacheServiceCallback(new CachedMuteStateChange(false));
+ assertEquals(1, call.getCachedServiceCallbacks().size());
+ call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+ assertEquals(2, call.getCachedServiceCallbacks().size());
+ call.cacheServiceCallback(new CachedAvailableEndpointsChange(availableEndpointsSet));
+ assertEquals(3, call.getCachedServiceCallbacks().size());
+
+ // verify the cached callbacks are stored properly within the cache map and the values
+ // can be evaluated
+ CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+ .getCachedServiceCallbacks()
+ .get(CachedMuteStateChange.ID);
+ CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+ .getCachedServiceCallbacks()
+ .get(CachedCurrentEndpointChange.ID);
+ CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+ .getCachedServiceCallbacks()
+ .get(CachedAvailableEndpointsChange.ID);
+ assertFalse(currentCacheMuteState.isMuted());
+ assertEquals(CallEndpoint.TYPE_EARPIECE,
+ currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+ assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+ // set the service to a non-null value
+ call.setTransactionServiceWrapper(tsw);
+
+ // ensure the cached callbacks were executed
+ verify(tsw, times(1)).onMuteStateChanged(any(), anyBoolean());
+ verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+ verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+
+ // the cache map should be cleared
+ assertEquals(0, call.getCachedServiceCallbacks().size());
+ }
+
+ /**
* Basic tests to check which call states are considered transitory.
*/
@Test
@@ -304,7 +524,6 @@
doReturn(true).when(mMockCallsManager).isInEmergencyCall();
call.pullExternalCall();
verify(mMockConnectionService, never()).pullExternalCall(any());
- verify(mMockToast).show();
}
@Test
@@ -473,6 +692,18 @@
@Test
@SmallTest
+ public void testGetFromCallerInfo_skipLookup() {
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+ .thenReturn(true);
+
+ createCall("1");
+
+ verify(mMockCallerInfoLookupHelper, never()).startLookup(any(), any());
+ }
+
+ @Test
+ @SmallTest
public void testOriginalCallIntent() {
Call call = createCall("1");
@@ -746,6 +977,24 @@
assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
}
+ /**
+ * Verify that a Call can handle a case where no telephony stack is present to detect emergency
+ * numbers.
+ */
+ @Test
+ @SmallTest
+ public void testNoTelephonyEmergencyBehavior() {
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+ .thenReturn(true);
+ Call testCall = createCall("1", Call.CALL_DIRECTION_OUTGOING, Uri.parse("tel:911"));
+ assertTrue(testCall.isEmergencyCall());
+
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+ .thenThrow(new UnsupportedOperationException("Bee-boop"));
+ Call testCall2 = createCall("2", Call.CALL_DIRECTION_OUTGOING, Uri.parse("tel:911"));
+ assertTrue(!testCall2.isEmergencyCall());
+ }
+
@Test
@SmallTest
public void testExcludesConnectionServiceWithoutModifyStatePermissionFromDoNotLogCallExtra() {
@@ -780,11 +1029,27 @@
assertTrue(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
}
+ @Test
+ @SmallTest
+ public void testSkipLoadingCannedTextResponse() {
+ Call call = createCall("any");
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+ .thenReturn(true);
+
+
+ assertFalse(call.isRespondViaSmsCapable());
+ }
+
private Call createCall(String id) {
return createCall(id, Call.CALL_DIRECTION_UNDEFINED);
}
private Call createCall(String id, int callDirection) {
+ return createCall(id, callDirection, TEST_ADDRESS);
+ }
+
+ private Call createCall(String id, int callDirection, Uri address) {
return new Call(
id,
mContext,
@@ -792,7 +1057,7 @@
mLock,
null,
mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
+ address,
null /* GatewayInfo */,
null,
SIM_1_HANDLE,
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index cb9aba9..ae5e6c1 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -33,9 +33,11 @@
import static org.mockito.ArgumentMatchers.anyString;
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.doNothing;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -57,6 +59,7 @@
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;
@@ -64,7 +67,7 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
import android.telecom.CallException;
import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
@@ -85,6 +88,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
+import com.android.internal.telecom.IConnectionService;
import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.AsyncRingtonePlayer;
import com.android.server.telecom.Call;
@@ -103,6 +107,7 @@
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;
@@ -298,7 +303,6 @@
@Mock private BluetoothStateReceiver mBluetoothStateReceiver;
@Mock private RoleManagerAdapter mRoleManagerAdapter;
@Mock private ToastFactory mToastFactory;
- @Mock private Toast mToast;
@Mock private CallAnomalyWatchdog mCallAnomalyWatchdog;
@Mock private EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
@@ -312,6 +316,9 @@
@Mock private FeatureFlags mFeatureFlags;
@Mock private com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
@Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
+ @Mock private Context mMockCreateContextAsUser;
+ @Mock private UserManager mMockCurrentUserManager;
+ @Mock private IConnectionService mIConnectionService;
private CallsManager mCallsManager;
@Override
@@ -403,14 +410,23 @@
eq(CALL_PROVIDER_HANDLE), any())).thenReturn(CALL_PROVIDER_ACCOUNT);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(WORK_HANDLE), any())).thenReturn(WORK_ACCOUNT);
- when(mToastFactory.makeText(any(), anyInt(), anyInt())).thenReturn(mToast);
- when(mToastFactory.makeText(any(), any(), anyInt())).thenReturn(mToast);
when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+ when(mContext.createContextAsUser(any(UserHandle.class), eq(0)))
+ .thenReturn(mMockCreateContextAsUser);
+ when(mMockCreateContextAsUser.getSystemService(UserManager.class))
+ .thenReturn(mMockCurrentUserManager);
+ 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();
}
@@ -1436,6 +1452,36 @@
verify(incomingCall).setIsUsingCallFiltering(eq(false));
}
+ /**
+ * Verify the ability to skip call filtering when Telephony reports we are in emergency SMS mode
+ * and also verify that when Telephony is not available we will not try to skip filtering.
+ */
+ @SmallTest
+ @Test
+ public void testFilteringWhenEmergencySmsCheckFails() {
+ // First see if it works when Telephony is present.
+ Call incomingCall = addSpyCall(CallState.NEW);
+ doReturn(true).when(mComponentContextFixture.getTelephonyManager()).isInEmergencySmsMode();
+ mCallsManager.onSuccessfulIncomingCall(incomingCall);
+ verify(incomingCall).setIsUsingCallFiltering(eq(false));
+
+ // Ensure when there is no telephony it doesn't try to skip filtering.
+ Call incomingCall2 = addSpyCall(CallState.NEW);
+ doThrow(new UnsupportedOperationException("Bee-boop")).when(
+ mComponentContextFixture.getTelephonyManager()).isInEmergencySmsMode();
+ mCallsManager.onSuccessfulIncomingCall(incomingCall2);
+ verify(incomingCall2).setIsUsingCallFiltering(eq(true));
+ }
+
+ @SmallTest
+ @Test
+ public void testDsdaAvailableCheckWhenNoTelephony() {
+ doThrow(new UnsupportedOperationException("Bee-boop")).when(
+ mComponentContextFixture.getTelephonyManager())
+ .getMaxNumberOfSimultaneouslyActiveSims();
+ assertFalse(mCallsManager.isDsdaCallingPossible());
+ }
+
@SmallTest
@Test
public void testNoFilteringOfNetworkIdentifiedEmergencyCalls() {
@@ -2723,6 +2769,24 @@
}
/**
+ * Verify when Telephony is not available we don't try to block redirection due to the failed
+ * isEmergency check.
+ */
+ @SmallTest
+ @Test
+ public void testEmergencyCheckFailsOnRedirectionCheckCompleteDueToNoTelephony() {
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+
+ Call callSpy = addSpyCall(CallState.NEW);
+ mCallsManager.onCallRedirectionComplete(callSpy, Uri.parse("tel:911"),
+ SIM_1_HANDLE_SECONDARY,
+ new GatewayInfo("foo", TEST_ADDRESS2, TEST_ADDRESS), true /* speakerphoneOn */,
+ VideoProfile.STATE_AUDIO_ONLY, false /* shouldCancelCall */, "" /* uiAction */);
+ verify(callSpy, never()).disconnect(anyString());
+ }
+
+ /**
* Verifies that target phone account is set in startOutgoingCall. The multi-user functionality
* is dependent on the call's phone account handle being present so this test ensures that
* existing outgoing call flow does not break from future updates.
@@ -2824,9 +2888,9 @@
mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
WORK_HANDLE.getUserHandle(), service);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isUserAdmin(anyInt())).thenReturn(false);
- when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false);
+ when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle())))
+ .thenReturn(false);
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
.thenReturn(WORK_ACCOUNT);
Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2845,9 +2909,9 @@
mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
WORK_HANDLE.getUserHandle(), service);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isUserAdmin(anyInt())).thenReturn(true);
- when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(true);
+ when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(true);
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
.thenReturn(WORK_ACCOUNT);
Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2868,8 +2932,8 @@
when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(SIM_2_HANDLE)))
.thenReturn(true);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle())))
+ .thenReturn(true);
Call newCall = mCallsManager.processIncomingCallIntent(
SIM_2_HANDLE, new Bundle(), false);
@@ -2887,9 +2951,9 @@
when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(WORK_HANDLE)))
.thenReturn(true);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isUserAdmin(anyInt())).thenReturn(false);
- when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false);
+ when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle())))
+ .thenReturn(false);
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
.thenReturn(WORK_ACCOUNT);
Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2910,8 +2974,8 @@
Bundle extras = new Bundle();
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, TEST_ADDRESS);
TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle())))
+ .thenReturn(true);
when(tm.isEmergencyNumber(any(String.class))).thenReturn(true);
Call newCall = mCallsManager.processIncomingCallIntent(
SIM_2_HANDLE, extras, false);
@@ -2974,42 +3038,152 @@
assertFalse(mCallsManager.getCalls().contains(call));
}
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
+ * on hold.
+ */
@MediumTest
@Test
- public void testHoldTransactional() throws Exception {
- CountDownLatch latch = new CountDownLatch(1);
+ public void testHoldWhenActiveCallIsNullOrSame() throws Exception {
Call newCall = addSpyCall();
-
// case 1: no active call, no need to put the call on hold
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(null);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, true));
- waitForCountDownLatch(latch);
-
+ assertHoldActiveCallForNewCall(
+ newCall,
+ null /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
// case 2: active call == new call, no need to put the call on hold
- latch = new CountDownLatch(1);
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(newCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, true));
- waitForCountDownLatch(latch);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ newCall /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
- // case 3: cannot hold current active call early check
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+ * cannot be held, and it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldFailsWithUnholdableCallAndCallControlRequest() throws Exception {
Call cannotHoldCall = addSpyCall(SIM_1_HANDLE, null,
CallState.ACTIVE, 0, 0);
- latch = new CountDownLatch(1);
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(cannotHoldCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, false));
- waitForCountDownLatch(latch);
+ assertHoldActiveCallForNewCall(
+ addSpyCall(),
+ cannotHoldCall /* activeCall */,
+ true /* isCallControlRequest */,
+ false /* expectOnResult */);
+ }
- // case 4: activeCall != newCall && canHold(activeCall)
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
+ * it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldSuccessWithHoldableActiveCall() throws Exception {
+ Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
Call canHoldCall = addSpyCall(SIM_1_HANDLE, null,
CallState.ACTIVE, Connection.CAPABILITY_HOLD, 0);
- latch = new CountDownLatch(1);
- when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(canHoldCall);
- mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
- new LatchedOutcomeReceiver(latch, true));
- waitForCountDownLatch(latch);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ canHoldCall /* activeCall */,
+ true /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+ * supports hold, and it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldWhenTheActiveCallSupportsHold() throws Exception {
+ Call newCall = addSpyCall();
+ Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, Connection.CAPABILITY_SUPPORT_HOLD, 0);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ supportsHold /* activeCall */,
+ true /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+ * supports hold + can hold, and it's a CallControlRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldWhenTheActiveCallSupportsAndCanHold() throws Exception {
+ Call newCall = addSpyCall();
+ Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE,
+ Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ supportsHold /* activeCall */,
+ true /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+ * supports hold + can hold, and it's a CallControlCallbackRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldForCallControlCallbackRequestWithActiveCallThatCanHold() throws Exception {
+ Call newCall = addSpyCall();
+ Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0);
+ assertHoldActiveCallForNewCall(
+ newCall,
+ supportsHold /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
+ }
+
+ /**
+ * Verify that
+ * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+ * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
+ * and it's a CallControlCallbackRequest.
+ */
+ @MediumTest
+ @Test
+ public void testHoldDisconnectsTheActiveCall() throws Exception {
+ Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
+ Call activeUnholdableCall = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, 0, 0);
+
+ doAnswer(invocation -> {
+ doReturn(true).when(activeUnholdableCall).isLocallyDisconnecting();
+ return null;
+ }).when(activeUnholdableCall).disconnect();
+
+ assertHoldActiveCallForNewCall(
+ newCall,
+ activeUnholdableCall /* activeCall */,
+ false /* isCallControlRequest */,
+ true /* expectOnResult */);
+
+ verify(activeUnholdableCall, atLeast(1)).disconnect();
}
@SmallTest
@@ -3073,6 +3247,35 @@
assertTrue(result.contains("onReceiveResult"));
}
+ @Test
+ public void testConnectionServiceCreateConnectionTimeout() throws Exception {
+ ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
+ mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
+ 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() {
@@ -3433,18 +3636,14 @@
when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
.thenReturn(true);
mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
- new Intent(
- BlockedNumberContract.BlockedNumbers
- .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+ new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
eq(true));
when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
.thenReturn(false);
mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
- new Intent(
- BlockedNumberContract.BlockedNumbers
- .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+ new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
eq(false));
}
@@ -3463,9 +3662,7 @@
// WHEN
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(any()))
.thenReturn(SM_W_DIFFERENT_PACKAGE_AND_USER);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isUserAdmin(eq(mCallsManager.getCurrentUserHandle().getIdentifier())))
- .thenReturn(true);
+ when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
// THEN
mCallsManager.processIncomingCallIntent(SELF_MANAGED_W_CUSTOM_HANDLE, new Bundle(), false);
@@ -3563,8 +3760,6 @@
.setShouldAllowCall(true)
.setShouldReject(false)
.build();
- when(mInCallController.bindToBTService(eq(call))).thenReturn(
- CompletableFuture.completedFuture(true));
when(mInCallController.isBoundAndConnectedToBTService(any(UserHandle.class)))
.thenReturn(false);
@@ -3572,7 +3767,7 @@
InOrder inOrder = inOrder(mInCallController, call, mInCallController);
- inOrder.verify(mInCallController).bindToBTService(eq(call));
+ inOrder.verify(mInCallController).bindToBTService(eq(call), eq(null));
inOrder.verify(call).setState(eq(CallState.RINGING), anyString());
}
@@ -3731,4 +3926,35 @@
when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability);
when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num);
}
+
+ private void assertHoldActiveCallForNewCall(
+ Call newCall,
+ Call activeCall,
+ boolean isCallControlRequest,
+ boolean expectOnResult)
+ throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+ newCall,
+ isCallControlRequest,
+ new LatchedOutcomeReceiver(latch, expectOnResult));
+ waitForCountDownLatch(latch);
+ }
+
+ 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 54aaa4c..25f94c6 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -73,6 +73,7 @@
import android.os.Vibrator;
import android.os.VibratorManager;
import android.permission.PermissionCheckerManager;
+import android.provider.BlockedNumbersManager;
import android.telecom.ConnectionService;
import android.telecom.Log;
import android.telecom.InCallService;
@@ -121,6 +122,7 @@
*/
public class ComponentContextFixture implements TestFixture<Context> {
private HandlerThread mHandlerThread;
+ private Map<UserHandle, Context> mContextsByUser = new HashMap<>();
public class FakeApplicationContext extends MockContext {
@Override
@@ -137,6 +139,9 @@
@Override
public Context createContextAsUser(UserHandle userHandle, int flags) {
+ if (mContextsByUser.containsKey(userHandle)) {
+ return mContextsByUser.get(userHandle);
+ }
return this;
}
@@ -251,6 +256,8 @@
return mSensorPrivacyManager;
case Context.ACCESSIBILITY_SERVICE:
return mAccessibilityManager;
+ case Context.BLOCKED_NUMBERS_SERVICE:
+ return mBlockedNumbersManager;
default:
return null;
}
@@ -292,6 +299,8 @@
return Context.BUGREPORT_SERVICE;
} else if (svcClass == TelecomManager.class) {
return Context.TELECOM_SERVICE;
+ } else if (svcClass == BlockedNumbersManager.class) {
+ return Context.BLOCKED_NUMBERS_SERVICE;
}
throw new UnsupportedOperationException(svcClass.getName());
}
@@ -635,6 +644,7 @@
private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
private TelecomManager mTelecomManager = mock(TelecomManager.class);
+ private BlockedNumbersManager mBlockedNumbersManager = mock(BlockedNumbersManager.class);
public ComponentContextFixture(FeatureFlags featureFlags) {
MockitoAnnotations.initMocks(this);
@@ -837,6 +847,10 @@
mSubscriptionManager = subscriptionManager;
}
+ public SubscriptionManager getSubscriptionManager() {
+ return mSubscriptionManager;
+ }
+
public TelephonyManager getTelephonyManager() {
return mTelephonyManager;
}
@@ -857,6 +871,19 @@
return mBroadcastReceivers;
}
+ public TelephonyRegistryManager getTelephonyRegistryManager() {
+ return mTelephonyRegistryManager;
+ }
+
+ /**
+ * For testing purposes, add a context for a specific user.
+ * @param userHandle the userhandle
+ * @param context the context
+ */
+ public void addContextForUser(UserHandle userHandle, Context context) {
+ mContextsByUser.put(userHandle, context);
+ }
+
private void addService(String action, ComponentName name, IInterface service) {
mComponentNamesByAction.put(action, name);
mServiceByComponentName.put(name, service);
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
new file mode 100644
index 0000000..c815e8e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceRepository;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ConnectionServiceWrapperTest extends TelecomTestCase {
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Verify we don't crash when getting the last known cell id and there is no telephony.
+ */
+ @Test
+ public void testGetLastKnownCellIdWhenNoTelephony() {
+ ConnectionServiceWrapper wrapper = new ConnectionServiceWrapper(
+ ComponentName.unflattenFromString("foo/baz"),
+ mock(ConnectionServiceRepository.class),
+ mock(PhoneAccountRegistrar.class),
+ mock(CallsManager.class),
+ mContext,
+ new TelecomSystem.SyncRoot() {},
+ UserHandle.CURRENT,
+ mock(FeatureFlags.class));
+ when(mComponentContextFixture.getTelephonyManager().getLastKnownCellIdentity())
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertNull(wrapper.getLastKnownCellIdentity());
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 2f27bb5..ddbc250 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,12 +16,16 @@
package com.android.server.telecom.tests;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.after;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -35,6 +39,7 @@
import android.net.Uri;
import android.os.Binder;
import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -42,18 +47,22 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.telephony.flags.Flags;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallIdMapper;
+import com.android.server.telecom.CallState;
import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ConnectionServiceRepository;
import com.android.server.telecom.ConnectionServiceWrapper;
import com.android.server.telecom.CreateConnectionProcessor;
import com.android.server.telecom.CreateConnectionResponse;
+import com.android.server.telecom.CreateConnectionTimeout;
import com.android.server.telecom.PhoneAccountRegistrar;
-import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.Timeouts;
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;
@@ -62,6 +71,7 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -73,6 +83,7 @@
*/
@RunWith(JUnit4.class)
public class CreateConnectionProcessorTest extends TelecomTestCase {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String TEST_PACKAGE = "com.android.server.telecom.tests";
private static final String TEST_CLASS =
@@ -89,12 +100,14 @@
Call mMockCall;
@Mock
ConnectionServiceFocusManager mConnectionServiceFocusManager;
+ @Mock Timeouts.Adapter mTimeoutsAdapter;
CreateConnectionProcessor mTestCreateConnectionProcessor;
+ CreateConnectionTimeout mTestCreateConnectionTimeout;
private ArrayList<PhoneAccount> phoneAccounts;
- private HashMap<Integer,Integer> mSubToSlot;
- private HashMap<PhoneAccount,Integer> mAccountToSub;
+ private HashMap<Integer, Integer> mSubToSlot;
+ private HashMap<PhoneAccount, Integer> mAccountToSub;
@Override
@Before
@@ -117,11 +130,11 @@
return null;
}
}
- ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
+ ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
mMockConnectionServiceRepository, mMockCreateConnectionResponse,
- mMockAccountRegistrar, mContext, mFeatureFlags);
+ mMockAccountRegistrar, mContext, mFeatureFlags, mTimeoutsAdapter);
mAccountToSub = new HashMap<>();
phoneAccounts = new ArrayList<>();
@@ -144,6 +157,11 @@
.thenReturn(phoneAccounts);
when(mMockCall.getAssociatedUser()).
thenReturn(Binder.getCallingUserHandle());
+
+ mTestCreateConnectionTimeout = new CreateConnectionTimeout(mContext, mMockAccountRegistrar,
+ makeConnectionServiceWrapper(), mMockCall, mTimeoutsAdapter);
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG);
}
@Override
@@ -300,7 +318,8 @@
/**
* Ensure that when a test emergency number is being dialed and we restrict the usable
- * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)}, the
+ * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)},
+ * the
* test emergency call is sent on the filtered PhoneAccount.
*/
@SmallTest
@@ -338,7 +357,8 @@
}
/**
- * Ensure that when no phone accounts (visible to the user) are available for the call, we use
+ * Ensure that when no phone accounts (visible to the user) are available for the call, we
+ * use
* an available sim from other another user (on the condition that the user has the
* INTERACT_ACROSS_USERS permission).
*/
@@ -368,7 +388,8 @@
}
/**
- * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to place
+ * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to
+ * place
* the emergency call if there is an emergency capable PhoneAccount available as well.
*/
@SmallTest
@@ -405,7 +426,8 @@
}
/**
- * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account to
+ * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account
+ * to
* dial the emergency call.
* 2) Register multiple emergency capable PhoneAccounts. Since there is not preference, we
* default to sending on the lowest slot.
@@ -445,8 +467,10 @@
}
/**
- * Ensure that the call goes out on the PhoneAccount that has the CAPABILITY_EMERGENCY_PREFERRED
- * capability, even if the user specifically chose the other emergency capable PhoneAccount.
+ * Ensure that the call goes out on the PhoneAccount that has the
+ * CAPABILITY_EMERGENCY_PREFERRED
+ * capability, even if the user specifically chose the other emergency capable
+ * PhoneAccount.
*/
@SmallTest
@Test
@@ -478,7 +502,8 @@
}
/**
- * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the user
+ * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the
+ * user
* chosen target account.
*/
@SmallTest
@@ -552,7 +577,8 @@
}
/**
- * If the user preferred PhoneAccount is associated with an invalid slot, place on the other,
+ * If the user preferred PhoneAccount is associated with an invalid slot, place on the
+ * other,
* valid slot.
*/
@SmallTest
@@ -690,7 +716,8 @@
/**
* Tests to verify that the
- * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)} can
+ * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)}
+ * can
* successfully sort without running into sort issues related to the hashcodes of the
* PhoneAccounts.
*/
@@ -707,6 +734,176 @@
}
}
+ @Test
+ public void testIsTimeoutNeededForCall_nonEmergencyCall() {
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+
+ assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(null, null));
+ }
+
+ @Test
+ public void testIsTimeoutNeededForCall_noConnectionManager() {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+ // Put in a regular phone account handle
+ PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ phoneAccountHandles.add(regularAccount.getAccountHandle());
+ // Create a connection manager for the call and do not include in phoneAccountHandles
+ createNewConnectionManagerPhoneAccountForCall(mMockCall, "cm_acct", 0);
+
+ assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+ }
+
+ @Test
+ public void testIsTimeoutNeededForCall_usingConnectionManager() {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+ // Put in a regular phone account handle
+ PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ phoneAccountHandles.add(regularAccount.getAccountHandle());
+ // Create a connection manager for the call and include it in phoneAccountHandles
+ PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+ "cm_acct", 0);
+ phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+ assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(
+ phoneAccountHandles, callManagerPA.getAccountHandle()));
+ }
+
+ @Test
+ public void testIsTimeoutNeededForCall_NotSystemSimCallManager() {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+ // Put in a regular phone account handle
+ PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ phoneAccountHandles.add(regularAccount.getAccountHandle());
+ // Create a connection manager for the call and include it in phoneAccountHandles
+ PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+ "cm_acct", 0);
+ phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+ assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+ }
+
+ @Test
+ public void testIsTimeoutNeededForCall() {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+ new ComponentName(TEST_PACKAGE, TEST_CLASS));
+
+ List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+ // Put in a regular phone account handle
+ PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ phoneAccountHandles.add(regularAccount.getAccountHandle());
+ // Create a connection manager for the call and include it in phoneAccountHandles
+ int capability = PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+ | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE;
+ PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+ "cm_acct", capability);
+ PhoneAccount phoneAccountWithService = makeQuickAccount("cm_acct", capability, null);
+ when(mMockAccountRegistrar.getPhoneAccount(callManagerPA.getAccountHandle(),
+ callManagerPA.getAccountHandle().getUserHandle()))
+ .thenReturn(phoneAccountWithService);
+ phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+ assertTrue(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+ }
+
+ @Test
+ public void testConnTimeout_carrierSatelliteEnabled_noInServiceConnManager_callNeverTimesOut() {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+ when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+ when(mMockCall.getState()).thenReturn(CallState.DIALING);
+ // Primary phone account, meant to fail
+ PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+ phoneAccounts.add(regularAccount);
+ when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ nullable(String.class))).thenReturn(regularAccount.getAccountHandle());
+ when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+ new ComponentName(TEST_PACKAGE, TEST_CLASS));
+ PhoneAccount callManagerPA = getNewEmergencyConnectionManagerPhoneAccount(
+ "cm_acct", PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS
+ | PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS);
+ phoneAccounts.add(callManagerPA);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ when(mMockAccountRegistrar.getSimCallManagerFromCall(mMockCall)).thenReturn(
+ callManagerPA.getAccountHandle());
+ when(mMockAccountRegistrar.getPhoneAccount(eq(callManagerPA.getAccountHandle()),
+ any())).thenReturn(callManagerPA);
+ Duration timeout = Duration.ofMillis(10);
+ when(mTimeoutsAdapter.getEmergencyCallTimeoutMillis(any())).thenReturn(timeout.toMillis());
+ when(mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(any())).thenReturn(
+ timeout.toMillis());
+
+
+ mTestCreateConnectionProcessor.process();
+
+ // Validate the call is not disconnected after the timeout.
+ verify(service, after(timeout.toMillis() + 100).never()).disconnect(eq(mMockCall));
+ }
+
+ @Test
+ public void testConnTimeout_carrierSatelliteEnabled_inServiceConnManager_callTimesOut() {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+ when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+ when(mMockCall.getState()).thenReturn(CallState.DIALING);
+ // Primary phone account, meant to fail
+ PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+ phoneAccounts.add(regularAccount);
+ when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ nullable(String.class))).thenReturn(regularAccount.getAccountHandle());
+ when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+ new ComponentName(TEST_PACKAGE, TEST_CLASS));
+ PhoneAccount callManagerPA = getNewEmergencyConnectionManagerPhoneAccount(
+ "cm_acct", PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS
+ | PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+ | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE
+ );
+ phoneAccounts.add(callManagerPA);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ when(mMockAccountRegistrar.getSimCallManagerFromCall(mMockCall)).thenReturn(
+ callManagerPA.getAccountHandle());
+ when(mMockAccountRegistrar.getPhoneAccount(eq(callManagerPA.getAccountHandle()),
+ any())).thenReturn(callManagerPA);
+ Duration timeout = Duration.ofMillis(10);
+ when(mTimeoutsAdapter.getEmergencyCallTimeoutMillis(any())).thenReturn(timeout.toMillis());
+ when(mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(any())).thenReturn(
+ timeout.toMillis());
+
+ mTestCreateConnectionProcessor.process();
+
+ // Validate the call was disconnected after the timeout.
+ verify(service, after(timeout.toMillis() + 100)).disconnect(eq(mMockCall));
+ }
+
+ /**
+ * Verifies when telephony is not available that we just get invalid sub id for a phone acct.
+ */
+ @SmallTest
+ @Test
+ public void testTelephonyAdapterWhenNoTelephony() {
+ PhoneAccount telephonyAcct = makePhoneAccount("test-acct",
+ PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+
+ CreateConnectionProcessor.ITelephonyManagerAdapterImpl impl
+ = new CreateConnectionProcessor.ITelephonyManagerAdapterImpl();
+ when(mComponentContextFixture.getTelephonyManager().
+ getSubscriptionId(any(PhoneAccountHandle.class)))
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertEquals(-1, impl.getSubIdForPhoneAccount(mContext, telephonyAcct));
+ }
+
/**
* Generates random phone accounts.
* @param seed random seed to use for random UUIDs; passed in for determinism.
diff --git a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
index 05c5071..3eacc54 100644
--- a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
@@ -146,6 +146,19 @@
verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), any());
}
+ /**
+ * Verifies when there is no telephony available, that we'll still be able to determine a
+ * country iso.
+ */
+ @Test
+ @SmallTest
+ public void testGetCountryIsoWithNoTelephony() {
+ DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+ when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertNotNull(notifier.getCurrentCountryIso(mContext));
+ }
+
private Call createCall(DisconnectCause cause) {
Call call = mock(Call.class);
when(call.getDisconnectCause()).thenReturn(cause);
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 3d99d07..6af31ae 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -154,10 +154,9 @@
@Mock NotificationManager mNotificationManager;
@Mock PermissionInfo mMockPermissionInfo;
@Mock InCallController.InCallServiceInfo mInCallServiceInfo;
- @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
@Mock UserManager mMockUserManager;
- @Mock UserInfo mMockUserInfo;
- @Mock UserInfo mMockChildUserInfo; //work profile
+ @Mock Context mMockCreateContextAsUser;
+ @Mock UserManager mMockCurrentUserManager;
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -226,7 +225,7 @@
when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYS_PKG);
when(mDefaultDialerCache.getSystemDialerComponent()).thenReturn(
new ComponentName(SYS_PKG, SYS_CLASS));
- when(mDefaultDialerCache.getBTInCallServicePackage()).thenReturn(BT_PKG);
+ when(mDefaultDialerCache.getBTInCallServicePackages()).thenReturn(new String[] {BT_PKG});
mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
mTimeoutsAdapter);
when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
@@ -312,10 +311,14 @@
when(mMockContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mMockUserManager);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
+ when(mMockContext.createContextAsUser(any(UserHandle.class), eq(0)))
+ .thenReturn(mMockCreateContextAsUser);
+ when(mMockCreateContextAsUser.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockCurrentUserManager);
// Mock user info to allow binding on user stored in the phone account (mUserHandle).
- when(mMockUserManager.getUserInfo(anyInt())).thenReturn(mMockUserInfo);
- when(mMockUserInfo.isManagedProfile()).thenReturn(true);
when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+ when(mMockCurrentUserManager.isManagedProfile()).thenReturn(true);
when(mFeatureFlags.profileUserSupport()).thenReturn(false);
}
@@ -406,7 +409,7 @@
.thenReturn(300_000L);
setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).bindServiceAsUser(
@@ -441,7 +444,7 @@
Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).bindServiceAsUser(
@@ -480,7 +483,7 @@
anyInt(), eq(mUserHandle))).thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -525,7 +528,8 @@
when(mMockCall.isEmergencyCall()).thenReturn(true);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -542,7 +546,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -599,11 +603,12 @@
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -629,11 +634,12 @@
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -660,12 +666,13 @@
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
- when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(false);
+ when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -688,12 +695,13 @@
when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
- when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(false);
+ when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
@@ -723,7 +731,8 @@
when(mMockCall.isEmergencyCall()).thenReturn(true);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -742,7 +751,7 @@
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -824,7 +833,7 @@
.thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -905,7 +914,7 @@
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -952,7 +961,7 @@
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// Now bind; we should only bind to one app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1001,7 +1010,8 @@
when(mMockCall.isEmergencyCall()).thenReturn(true);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
@@ -1034,7 +1044,7 @@
.thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
mInCallController.addCall(mMockCall);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// There will be 4 calls for the various types of ICS.
verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
@@ -1111,7 +1121,8 @@
when(mMockCall.isEmergencyCall()).thenReturn(true);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -1202,7 +1213,7 @@
public void testBindToService_IncludeExternal() throws Exception {
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1253,7 +1264,7 @@
when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1302,7 +1313,7 @@
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// Now bind; we should only bind to one app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1321,7 +1332,7 @@
public void testNoBindToInvalidService_CarModeUI() throws Exception {
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
when(mMockPackageManager.checkPermission(
matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
@@ -1373,7 +1384,7 @@
anyInt(), any(AttributionSource.class), nullable(String.class)));
// Now bind; we should bind to the system dialer and app op non ui app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1417,7 +1428,7 @@
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(null);
// we should bind to only the non ui app.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1450,7 +1461,7 @@
matches(DEF_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
when(mMockCall.getName()).thenReturn("evil");
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1487,7 +1498,7 @@
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
// Bind to default dialer.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Uninstall an unrelated app.
mSystemStateListener.onPackageUninstalled("com.joe.stuff");
@@ -1511,7 +1522,7 @@
setupMocks(true /* isExternalCall */);
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
// Bind to default dialer.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Enable car mode and enter car mode at default priority.
when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
@@ -1579,7 +1590,7 @@
setupMockPackageManager(true /* default */, true /* nonui */, false /* appop_nonui */ ,
true /* system */, false /* external calls */,
false /* self mgd in default*/, false /* self mgd in car*/);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1648,7 +1659,7 @@
// Bind; we should not bind to anything right now; the dialer does not support self
// managed calls.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices; make sure no binding took place. InCallController handles not
// binding initially, but the rebind (see next test case) will always happen.
@@ -1687,7 +1698,7 @@
// Bind; we should not bind to anything right now; the dialer does not support self
// managed calls.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallServices; make sure no binding took place.
verify(mMockContext, never()).bindServiceAsUser(
@@ -1787,9 +1798,9 @@
// Force the difference between the phone account user and current user. This is supposed to
// simulate a secondary user placing a call over an unassociated sim.
assertFalse(mUserHandle.equals(UserHandle.USER_CURRENT));
- when(mMockUserInfo.isManagedProfile()).thenReturn(false);
+ when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
// Bind InCallService on UserHandle.CURRENT and not the user from the call (mUserHandle)
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1810,7 +1821,7 @@
when(mMockCall.getAssociatedUser()).thenReturn(testUser);
// Bind to ICS. The mapping should've been inserted with the testUser as the key.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
// Set the target phone account. Simulates the flow when the user has chosen which sim to
@@ -1838,7 +1849,7 @@
when(mMockCall.isIncoming()).thenReturn(true);
// Bind to ICS. The mapping should've been inserted with the testUser as the key.
- mInCallController.bindToServices(mMockCall, false);
+ mInCallController.bindToServices(mMockCall);
assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
// Remove the call. This invokes getUserFromCall to remove the ICS mapping.
@@ -1856,9 +1867,7 @@
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(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
when(mMockCall.getAssociatedUser()).thenReturn(workUser);
setupFakeSystemCall(mMockSystemCall1, 1);
setupFakeSystemCall(mMockSystemCall2, 2);
@@ -1924,21 +1933,9 @@
when(mMockChildUserCall.visibleToInCallService()).thenReturn(true);
//Setup up parent and child/work profile relation
- when(mMockUserInfo.getUserHandle()).thenReturn(mParentUserHandle);
- when(mMockChildUserInfo.getUserHandle()).thenReturn(mChildUserHandle);
- when(mMockUserInfo.isManagedProfile()).thenReturn(false);
- when(mMockChildUserInfo.isManagedProfile()).thenReturn(false);
when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
- when(mMockUserManager.getProfileParent(mChildUserHandle.getIdentifier())).thenReturn(
- mMockUserInfo);
when(mMockUserManager.getProfileParent(mChildUserHandle)).thenReturn(mParentUserHandle);
- when(mMockUserManager.getUserInfo(eq(mParentUserHandle.getIdentifier()))).thenReturn(
- mMockUserInfo);
- when(mMockUserManager.getUserInfo(eq(mChildUserHandle.getIdentifier()))).thenReturn(
- mMockChildUserInfo);
- when(mMockUserManager.isManagedProfile(mParentUserHandle.getIdentifier())).thenReturn(
- false);
when(mFeatureFlags.profileUserSupport()).thenReturn(true);
}
@@ -1954,7 +1951,7 @@
true /*includeSelfManagedCallsInNonUi*/);
//pass in call by child/profile user
- mInCallController.bindToServices(mMockChildUserCall, false);
+ mInCallController.bindToServices(mMockChildUserCall);
// Verify that queryIntentServicesAsUser is also called with parent handle
// Query for the different InCallServices
ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1974,8 +1971,10 @@
@Test
public void testSeparatelyBluetoothService() {
+ setupMocks(false /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
Intent expectedIntent = new Intent(InCallService.SERVICE_INTERFACE);
- expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackage());
+ expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackages()[0]);
LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
resolveInfo.add(getBluetoothResolveinfo());
when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
@@ -1991,7 +1990,7 @@
}).when(mMockPackageManager).queryIntentServicesAsUser(any(Intent.class), anyInt(),
anyInt());
- mInCallController.bindToBTService(mMockCall);
+ mInCallController.bindToBTService(mMockCall, null);
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).bindServiceAsUser(captor.capture(), any(ServiceConnection.class),
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 61e8347..1776411 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -692,6 +692,25 @@
nullable(Notification.class), eq(PRIMARY_USER));
}
+ /**
+ * Ensure when Telephony is not present on a device and getNetworkCountryIso throws an
+ * unsupported operation exception that we will still fallback to the device locale.
+ */
+ @SmallTest
+ @Test
+ public void testGetCountryIsoWithNoTelephony() {
+ Notification.Builder builder1 = makeNotificationBuilder("builder1");
+ MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+ makeNotificationBuilderFactory(builder1);
+ MissedCallNotifierImpl missedCallNotifier = new MissedCallNotifierImpl(mContext,
+ mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+ mDeviceIdleControllerAdapter, mFeatureFlags);
+
+ when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertNotNull(missedCallNotifier.getCurrentCountryIso(mContext));
+ }
+
private Notification.Builder makeNotificationBuilder(String label) {
Notification.Builder builder = spy(new Notification.Builder(mContext));
Notification notification = mock(Notification.class);
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 0ce5836..9d87aaf 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -29,6 +29,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -138,7 +139,7 @@
.thenReturn(TEST_LABEL);
mRegistrar = new PhoneAccountRegistrar(
mComponentContextFixture.getTestDouble().getApplicationContext(), mLock, FILE_NAME,
- mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags);
+ mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags, mFeatureFlags);
when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
}
@@ -159,12 +160,14 @@
public void testPhoneAccountHandle() throws Exception {
PhoneAccountHandle input = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id0");
PhoneAccountHandle result = roundTripXml(this, input,
- PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext, mTelephonyFeatureFlags);
+ PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+ mTelephonyFeatureFlags, mFeatureFlags);
assertPhoneAccountHandleEquals(input, result);
PhoneAccountHandle inputN = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), null);
PhoneAccountHandle resultN = roundTripXml(this, inputN,
- PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext, mTelephonyFeatureFlags);
+ PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+ mTelephonyFeatureFlags, mFeatureFlags);
Log.i(this, "inputN = %s, resultN = %s", inputN, resultN);
assertPhoneAccountHandleEquals(inputN, resultN);
}
@@ -187,7 +190,7 @@
.setIsEnabled(true)
.build();
PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
- mContext, mTelephonyFeatureFlags);
+ mContext, mTelephonyFeatureFlags, mFeatureFlags);
assertPhoneAccountEquals(input, result);
}
@@ -198,7 +201,7 @@
doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
// workaround: UserManager converts the user to a serial and back, we need to mock this
// behavior, unfortunately: USER_HANDLE_10 <-> 10L
- UserManager userManager = UserManager.get(mContext);
+ UserManager userManager = mContext.getSystemService(UserManager.class);
doReturn(10L).when(userManager).getSerialNumberForUser(eq(USER_HANDLE_10));
doReturn(USER_HANDLE_10).when(userManager).getUserForSerialNumber(eq(10L));
Bundle testBundle = new Bundle();
@@ -222,7 +225,7 @@
.setSimultaneousCallingRestriction(restriction)
.build();
PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
- mContext, mTelephonyFeatureFlags);
+ mContext, mTelephonyFeatureFlags, mFeatureFlags);
assertPhoneAccountEquals(input, result);
}
@@ -234,7 +237,7 @@
doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
// workaround: UserManager converts the user to a serial and back, we need to mock this
// behavior, unfortunately: USER_HANDLE_10 <-> 10L
- UserManager userManager = UserManager.get(mContext);
+ UserManager userManager = mContext.getSystemService(UserManager.class);
doReturn(10L).when(userManager).getSerialNumberForUser(eq(USER_HANDLE_10));
doReturn(USER_HANDLE_10).when(userManager).getUserForSerialNumber(eq(10L));
Bundle testBundle = new Bundle();
@@ -262,7 +265,7 @@
// Simulate turning off the flag after reboot
doReturn(false).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
- mTelephonyFeatureFlags);
+ mTelephonyFeatureFlags, mFeatureFlags);
assertNotNull(result);
assertFalse(result.hasSimultaneousCallingRestriction());
@@ -292,7 +295,7 @@
// Simulate turning on the flag after reboot
doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
- mTelephonyFeatureFlags);
+ mTelephonyFeatureFlags, mFeatureFlags);
assertPhoneAccountEquals(input, result);
}
@@ -364,13 +367,14 @@
public void testDefaultPhoneAccountHandleEmptyGroup() throws Exception {
DefaultPhoneAccountHandle input = new DefaultPhoneAccountHandle(Process.myUserHandle(),
makeQuickAccountHandle("i1"), "");
- when(UserManager.get(mContext).getSerialNumberForUser(input.userHandle))
+ UserManager userManager = mContext.getSystemService(UserManager.class);
+ when(userManager.getSerialNumberForUser(input.userHandle))
.thenReturn(0L);
- when(UserManager.get(mContext).getUserForSerialNumber(0L))
+ when(userManager.getUserForSerialNumber(0L))
.thenReturn(input.userHandle);
DefaultPhoneAccountHandle result = roundTripXml(this, input,
PhoneAccountRegistrar.sDefaultPhoneAccountHandleXml, mContext,
- mTelephonyFeatureFlags);
+ mTelephonyFeatureFlags, mFeatureFlags);
assertDefaultPhoneAccountHandleEquals(input, result);
}
@@ -400,7 +404,7 @@
.setExtras(testBundle)
.build();
PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
- mContext, mTelephonyFeatureFlags);
+ mContext, mTelephonyFeatureFlags, mFeatureFlags);
Bundle extras = result.getExtras();
assertFalse(extras.keySet().contains("EXTRA_STR2"));
@@ -414,7 +418,7 @@
public void testState() throws Exception {
PhoneAccountRegistrar.State input = makeQuickState();
PhoneAccountRegistrar.State result = roundTripXml(this, input,
- PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags);
+ PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags, mFeatureFlags);
assertStateEquals(input, result);
}
@@ -1245,6 +1249,7 @@
// GIVEN
mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
Mockito.mock(IConnectionService.class));
+ UserManager userManager = mContext.getSystemService(UserManager.class);
List<UserHandle> users = Arrays.asList(new UserHandle(0),
new UserHandle(1000));
@@ -1271,10 +1276,10 @@
when(mContext.getPackageManager().getPackageInfo(PACKAGE_2, 0))
.thenThrow(new PackageManager.NameNotFoundException());
- when(UserManager.get(mContext).getSerialNumberForUser(users.get(0)))
+ when(userManager.getSerialNumberForUser(users.get(0)))
.thenReturn(0L);
- when(UserManager.get(mContext).getSerialNumberForUser(users.get(1)))
+ when(userManager.getSerialNumberForUser(users.get(1)))
.thenReturn(-1L);
// THEN
@@ -1865,7 +1870,7 @@
makeQuickAccountHandle(TEST_ID)).setIcon(mockIcon);
try {
// WHEN
- Mockito.doThrow(new IOException())
+ doThrow(new IOException())
.when(mockIcon).writeToStream(any(OutputStream.class));
//THEN
mRegistrar.enforceIconSizeLimit(builder.build());
@@ -1961,6 +1966,36 @@
assertTrue(accountsForUser.contains(accountForAll));
}
+ @SmallTest
+ @Test
+ public void testGetSubscriptionIdForPhoneAccountWhenNoTelephony() throws Exception {
+ mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+
+ PhoneAccount simAccount =
+ makeQuickAccountBuilder("simzor", 1, null)
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ .setIsEnabled(true)
+ .build();
+ registerAndEnableAccount(simAccount);
+ when(mComponentContextFixture.getTelephonyManager()
+ .getSubscriptionId(any(PhoneAccountHandle.class)))
+ .thenThrow(new UnsupportedOperationException("Bee-boop"));
+ assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ mRegistrar.getSubscriptionIdForPhoneAccount(simAccount.getAccountHandle()));
+
+ // One more thing; we'll test
+ doThrow(new UnsupportedOperationException("Bee boop!"))
+ .when(mComponentContextFixture.getSubscriptionManager())
+ .setDefaultVoiceSubscriptionId(anyInt());
+ mRegistrar.setUserSelectedOutgoingPhoneAccount(simAccount.getAccountHandle(),
+ simAccount.getAccountHandle().getUserHandle());
+
+ // There is nothing to verify, we just want to ensure that we didn't crash.
+ }
+
private static PhoneAccount.Builder makeBuilderWithBindCapabilities(PhoneAccountHandle handle) {
return new PhoneAccount.Builder(handle, TEST_LABEL)
.setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
@@ -2080,7 +2115,8 @@
T input,
PhoneAccountRegistrar.XmlSerialization<T> xml,
Context context,
- FeatureFlags telephonyFeatureFlags)
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws Exception {
Log.d(self, "Input = %s", input);
@@ -2088,7 +2124,7 @@
Log.i(self, "====== XML data ======\n%s", new String(data));
- T result = fromXml(data, xml, context, telephonyFeatureFlags);
+ T result = fromXml(data, xml, context, telephonyFeatureFlags, telecomFeatureFlags);
Log.i(self, "result = " + result);
@@ -2106,11 +2142,13 @@
}
private static <T> T fromXml(byte[] data, PhoneAccountRegistrar.XmlSerialization<T> xml,
- Context context, FeatureFlags telephonyFeatureFlags) throws Exception {
+ Context context, FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) throws Exception {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new BufferedInputStream(new ByteArrayInputStream(data)), null);
parser.nextTag();
- return xml.readFromXml(parser, MAX_VERSION, context, telephonyFeatureFlags);
+ return xml.readFromXml(parser, MAX_VERSION, context,
+ telephonyFeatureFlags, telecomFeatureFlags);
}
@@ -2208,6 +2246,7 @@
}
private PhoneAccountRegistrar.State makeQuickStateWithTelephonyPhoneAccountHandle() {
+ UserManager userManager = mContext.getSystemService(UserManager.class);
PhoneAccountRegistrar.State s = new PhoneAccountRegistrar.State();
s.accounts.add(makeQuickAccount("id0", 0));
s.accounts.add(makeQuickAccount("id1", 1));
@@ -2216,9 +2255,9 @@
"com.android.phone",
"com.android.services.telephony.TelephonyConnectionService"), "id0");
UserHandle userHandle = phoneAccountHandle.getUserHandle();
- when(UserManager.get(mContext).getSerialNumberForUser(userHandle))
+ when(userManager.getSerialNumberForUser(userHandle))
.thenReturn(0L);
- when(UserManager.get(mContext).getUserForSerialNumber(0L))
+ when(userManager.getUserForSerialNumber(0L))
.thenReturn(userHandle);
s.defaultOutgoingAccountHandles
.put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
@@ -2227,6 +2266,7 @@
}
private PhoneAccountRegistrar.State makeQuickState() {
+ UserManager userManager = mContext.getSystemService(UserManager.class);
PhoneAccountRegistrar.State s = new PhoneAccountRegistrar.State();
s.accounts.add(makeQuickAccount("id0", 0));
s.accounts.add(makeQuickAccount("id1", 1));
@@ -2234,9 +2274,9 @@
PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(
new ComponentName("pkg0", "cls0"), "id0");
UserHandle userHandle = phoneAccountHandle.getUserHandle();
- when(UserManager.get(mContext).getSerialNumberForUser(userHandle))
+ when(userManager.getSerialNumberForUser(userHandle))
.thenReturn(0L);
- when(UserManager.get(mContext).getUserForSerialNumber(0L))
+ when(userManager.getUserForSerialNumber(0L))
.thenReturn(userHandle);
s.defaultOutgoingAccountHandles
.put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
diff --git a/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
new file mode 100644
index 0000000..b18c2ce
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.util.ArrayMap;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneStateBroadcaster;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class PhoneStateBroadcasterTest extends TelecomTestCase {
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Tests behavior where FEATURE_TELEPHONY_CALLING is not available, but
+ * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can't detect that the number is emergency
+ * so we will not bother sending out anything.
+ */
+ @Test
+ public void testNotifyOutgoingEmergencyCallWithNoTelephonyCalling() {
+ CallsManager cm = mock(CallsManager.class);
+ when(cm.getContext()).thenReturn(mContext);
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+ Call call = mock(Call.class);
+ when(call.isExternalCall()).thenReturn(false);
+ when(call.isEmergencyCall()).thenReturn(true);
+ when(call.isIncoming()).thenReturn(false);
+ when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+
+ psb.onCallAdded(call);
+ verify(mComponentContextFixture.getTelephonyRegistryManager(), never())
+ .notifyOutgoingEmergencyCall(anyInt(), anyInt(), any(EmergencyNumber.class));
+ }
+
+ /**
+ * Tests behavior where FEATURE_TELEPHONY_CALLING is available, but
+ * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can detect that this is an emergency
+ * call, but we can't figure out any of the subscription parameters. It is doubtful we'd ever
+ * see this in practice since technically FEATURE_TELEPHONY_CALLING needs
+ * FEATURE_TELEPHONY_SUBSCRIPTION.
+ */
+ @Test
+ public void testNotifyOutgoingEmergencyCallWithNoTelephonySubscription() {
+ CallsManager cm = mock(CallsManager.class);
+ when(cm.getContext()).thenReturn(mContext);
+ Map<Integer, List<EmergencyNumber>> nums = new ArrayMap<Integer, List<EmergencyNumber>>();
+ nums.put(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ Arrays.asList(new EmergencyNumber("911", "US", null, 0, Collections.EMPTY_LIST,
+ 0, 0)));
+ when(mComponentContextFixture.getTelephonyManager().getEmergencyNumberList())
+ .thenReturn(nums);
+ when(mComponentContextFixture.getTelephonyManager().getSubscriptionId(any(
+ PhoneAccountHandle.class)))
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+ Call call = mock(Call.class);
+ when(call.isExternalCall()).thenReturn(false);
+ when(call.isEmergencyCall()).thenReturn(true);
+ when(call.isIncoming()).thenReturn(false);
+ when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+ when(call.getTargetPhoneAccount()).thenReturn(new PhoneAccountHandle(
+ ComponentName.unflattenFromString("foo/bar"), "90210"));
+
+ psb.onCallAdded(call);
+ verify(mComponentContextFixture.getTelephonyRegistryManager())
+ .notifyOutgoingEmergencyCall(eq(SubscriptionManager.INVALID_SIM_SLOT_INDEX),
+ eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+ any(EmergencyNumber.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index a36e8ea..dc5f325 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -1167,7 +1167,7 @@
verify(mFakePhoneAccountRegistrar).getPhoneAccount(
TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
- verify(mInCallController, never()).bindToServices(any(), anyBoolean());
+ verify(mInCallController, never()).bindToServices(any());
addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
TEL_PA_HANDLE_16, false);
@@ -1189,7 +1189,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController, never()).bindToServices(eq(null));
}
@SmallTest
@@ -1207,7 +1207,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController).bindToServices(eq(null));
}
@SmallTest
@@ -1225,7 +1225,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController, never()).bindToServices(eq(null));
}
@SmallTest
@@ -1244,7 +1244,7 @@
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
- verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+ verify(mInCallController, never()).bindToServices(eq(null));
}
@@ -2092,6 +2092,23 @@
mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE, null));
}
+ /**
+ * Verify that when Telephony is not present that getLine1Number returns null as expected.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testGetLine1NumberWithNoTelephony() throws Exception {
+ setupGetLine1NumberTest();
+ grantPermissionAndAppOp(READ_PHONE_NUMBERS, AppOpsManager.OPSTR_READ_PHONE_NUMBERS);
+ TelephonyManager mockTelephonyManager =
+ (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ when(mockTelephonyManager.getLine1Number()).thenThrow(
+ new UnsupportedOperationException("Bee-boop"));
+
+ assertNull(mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE, null));
+ }
+
private String setupGetLine1NumberTest() throws Exception {
int subId = 58374;
String line1Number = "9482752023479";
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 57802e3..4463d65 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -516,6 +516,7 @@
when(mRoleManagerAdapter.getCallCompanionApps()).thenReturn(Collections.emptyList());
when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class)))
.thenReturn(null);
+ when(mRoleManagerAdapter.getBTInCallService()).thenReturn(new String[] {"bt_pkg"});
when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
mTelecomSystem = new TelecomSystem(
mComponentContextFixture.getTestDouble(),
@@ -803,7 +804,7 @@
final UserHandle userHandle = initiatingUser;
Context localAppContext = mComponentContextFixture.getTestDouble().getApplicationContext();
- new UserCallIntentProcessor(localAppContext, userHandle).processIntent(
+ new UserCallIntentProcessor(localAppContext, userHandle, mFeatureFlags).processIntent(
actionCallIntent, null, false, true /* hasCallAppOp*/, false /* isLocal */);
// Wait for handler to start CallerInfo lookup.
waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java b/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java
new file mode 100644
index 0000000..207da71
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import com.android.server.telecom.TelephonyUtil;
+
+import android.net.Uri;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TelephonyUtilTest extends TelecomTestCase {
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Verifies that the helper method shouldProcessAsEmergency does not crash when telephony is not
+ * present and returns "false" instead.
+ */
+ @Test
+ public void testShouldProcessAsEmergencyWithNoTelephonyCalling() {
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+ .thenThrow(new UnsupportedOperationException("Bee boop"));
+ assertFalse(TelephonyUtil.shouldProcessAsEmergency(mContext, Uri.parse("tel:911")));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 0f7fd48..5876474 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,12 +16,17 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
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.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.isA;
@@ -35,6 +40,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.OutcomeReceiver;
@@ -42,6 +48,8 @@
import android.telecom.CallAttributes;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
import androidx.test.filters.SmallTest;
@@ -61,7 +69,9 @@
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.TransactionManager;
import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VideoStateTranslation;
import com.android.server.telecom.voip.VoipCallTransactionResult;
import org.junit.After;
@@ -105,6 +115,7 @@
super.setUp();
MockitoAnnotations.initMocks(this);
Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
}
@Override
@@ -206,14 +217,14 @@
public void testTransactionalHoldActiveCallForNewCall() throws Exception {
// GIVEN
MaybeHoldCallForNewCallTransaction transaction =
- new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1);
+ new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1, false);
// WHEN
transaction.processTransaction(null);
// THEN
verify(mCallsManager, times(1))
- .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1),
+ .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
isA(OutcomeReceiver.class));
}
@@ -224,7 +235,8 @@
CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
OutgoingCallTransaction transaction =
- new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+ new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager,
+ mFeatureFlags);
// WHEN
when(mMockContext.getOpPackageName()).thenReturn("testPackage");
@@ -251,7 +263,8 @@
CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
IncomingCallTransaction transaction =
- new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+ new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager,
+ mFeatureFlags);
// WHEN
when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
@@ -266,32 +279,123 @@
}
/**
+ * Verify that transactional OUTGOING calls are re-mapping the CallAttributes video state to
+ * VideoProfile states when starting the call via CallsManager#startOugoingCall.
+ */
+ @Test
+ public void testOutgoingCallTransactionRemapsVideoState() {
+ // GIVEN
+ CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.VIDEO_CALL)
+ .build();
+
+ OutgoingCallTransaction t = new OutgoingCallTransaction(null,
+ mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+
+ // WHEN
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+ t.setFeatureFlags(mFeatureFlags);
+
+ // THEN
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+ .generateExtras(audioOnlyAttributes)
+ .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+ .generateExtras(videoAttributes)
+ .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+ }
+
+ /**
+ * Verify that transactional INCOMING calls are re-mapping the CallAttributes video state to
+ * VideoProfile states when starting the call in CallsManager#processIncomingCallIntent.
+ */
+ @Test
+ public void testIncomingCallTransactionRemapsVideoState() {
+ // GIVEN
+ CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.VIDEO_CALL)
+ .build();
+
+ IncomingCallTransaction t = new IncomingCallTransaction(null, null,
+ mCallsManager, new Bundle(), mFeatureFlags);
+
+ // WHEN
+ when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+ t.setFeatureFlags(mFeatureFlags);
+
+ // THEN
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+ .generateExtras(audioOnlyAttributes)
+ .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+ .generateExtras(videoAttributes)
+ .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+ }
+
+ @Test
+ public void testTransactionalVideoStateToVideoProfileState() {
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+ TransactionalVideoStateToVideoProfileState(CallAttributes.AUDIO_CALL));
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+ TransactionalVideoStateToVideoProfileState(CallAttributes.VIDEO_CALL));
+ // ensure non-defined values default to audio
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+ TransactionalVideoStateToVideoProfileState(-1));
+ }
+
+ @Test
+ public void testVideoProfileStateToTransactionalVideoState() {
+ assertEquals(CallAttributes.AUDIO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_AUDIO_ONLY));
+ assertEquals(CallAttributes.VIDEO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_RX_ENABLED));
+ assertEquals(CallAttributes.VIDEO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_TX_ENABLED));
+ assertEquals(CallAttributes.VIDEO_CALL,
+ VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_BIDIRECTIONAL));
+ // ensure non-defined values default to audio
+ assertEquals(CallAttributes.AUDIO_CALL,
+ VideoProfileStateToTransactionalVideoState(-1));
+ }
+
+ /**
* 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 {
+ public void testCallStateChangeTimesOut() {
when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
- VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
- mMockCall1, CallState.ON_HOLD, true);
+ VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+ mLock, mMockCall1, CallState.ON_HOLD);
+ TransactionManager.TransactionCompleteListener listener =
+ mock(TransactionManager.TransactionCompleteListener.class);
+ t.setCompleteListener(listener);
// 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);
+ t.timeout();
// 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(listener).onTransactionTimeout(anyString());
verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
- verify(mCallsManager, times(1)).markCallAsDisconnected(eq(mMockCall1), any());
- verify(mCallsManager, times(1)).markCallAsRemoved(eq(mMockCall1));
}
/**
@@ -303,25 +407,23 @@
public void testCallStateIsSuccessfullyChanged()
throws ExecutionException, InterruptedException, TimeoutException {
when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
- VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
- mMockCall1, CallState.ON_HOLD, true);
+ VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+ mLock, mMockCall1, CallState.ON_HOLD);
// WHEN
setupHoldableCall();
// simulate the transaction being processed and the setOnHold() being called / state change
t.processTransaction(null);
- when(mMockCall1.getState()).thenReturn(CallState.ON_HOLD);
+ doReturn(CallState.ON_HOLD).when(mMockCall1).getState();
t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD);
+ t.finish(null);
+
// 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) {
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index b7848a2..c5be130 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -21,7 +21,6 @@
import android.os.OutcomeReceiver;
import android.telecom.CallException;
-import android.util.Log;
import androidx.test.filters.SmallTest;
@@ -61,6 +60,7 @@
private long mSleepTime;
private String mName;
private int mType;
+ public boolean isFinished = false;
public TestVoipCallTransaction(String name, long sleepTime, int type) {
super(VoipCallTransactionTest.this.mLock);
@@ -85,17 +85,22 @@
} else if (mType == FAILED) {
mLog.append(mName).append(" failed;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
null));
} else {
mLog.append(mName).append(" timeout;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
"timeout"));
}
}, mSleepTime);
return resultFuture;
}
+
+ @Override
+ public void finishTransaction() {
+ isFinished = true;
+ }
}
@Override
@@ -109,7 +114,6 @@
@Override
@After
public void tearDown() throws Exception {
- Log.i("Grace", mLog.toString());
mTransactionManager.clear();
super.tearDown();
}
@@ -119,11 +123,11 @@
public void testSerialTransactionSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+ TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
TestVoipCallTransaction.SUCCESS);
subTransactions.add(t1);
subTransactions.add(t2);
@@ -137,6 +141,7 @@
assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
assertEquals(expectedLog, mLog.toString());
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@@ -144,11 +149,11 @@
public void testSerialTransactionFailed()
throws ExecutionException, InterruptedException, TimeoutException {
List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
TestVoipCallTransaction.FAILED);
- VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+ TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
TestVoipCallTransaction.SUCCESS);
subTransactions.add(t1);
subTransactions.add(t2);
@@ -171,6 +176,7 @@
exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
String expectedLog = "t1 success;\nt2 failed;\n";
assertEquals(expectedLog, mLog.toString());
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@@ -178,11 +184,11 @@
public void testParallelTransactionSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+ TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
TestVoipCallTransaction.SUCCESS);
subTransactions.add(t1);
subTransactions.add(t2);
@@ -198,6 +204,7 @@
assertTrue(log.contains("t1 success;\n"));
assertTrue(log.contains("t2 success;\n"));
assertTrue(log.contains("t3 success;\n"));
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@@ -205,11 +212,11 @@
public void testParallelTransactionFailed()
throws ExecutionException, InterruptedException, TimeoutException {
List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
TestVoipCallTransaction.FAILED);
- VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+ TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
TestVoipCallTransaction.SUCCESS);
subTransactions.add(t1);
subTransactions.add(t2);
@@ -231,13 +238,14 @@
outcomeReceiver);
exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
assertTrue(mLog.toString().contains("t2 failed;\n"));
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@Test
public void testTransactionTimeout()
throws ExecutionException, InterruptedException, TimeoutException {
- VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
+ TestVoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
TestVoipCallTransaction.SUCCESS);
CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
@@ -255,15 +263,16 @@
mTransactionManager.addTransaction(t, outcomeReceiver);
String message = exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
assertTrue(message.contains("timeout"));
+ verifyTransactionsFinished(t);
}
@SmallTest
@Test
public void testTransactionException()
throws ExecutionException, InterruptedException, TimeoutException {
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
TestVoipCallTransaction.EXCEPTION);
- VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
TestVoipCallTransaction.SUCCESS);
CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
@@ -290,17 +299,52 @@
assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
assertEquals(expectedLog, mLog.toString());
+ verifyTransactionsFinished(t1, t2);
+ }
+
+ /**
+ * This test verifies that if a transaction encounters an exception while processing it,
+ * the exception finishes the transaction immediately instead of waiting for the timeout.
+ */
+ @SmallTest
+ @Test
+ public void testTransactionHitsException()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ // GIVEN - a transaction that throws an exception when processing
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction(
+ "t1",
+ 100L,
+ TestVoipCallTransaction.EXCEPTION);
+ // verify the TransactionManager informs the client of the failed transaction
+ CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ }
+
+ @Override
+ public void onError(CallException e) {
+ exceptionFuture.complete(e.getMessage());
+ }
+ };
+ // WHEN - add and process the transaction
+ mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+ exceptionFuture.get(200L, TimeUnit.MILLISECONDS);
+ // THEN - assert the transaction finished and failed
+ assertTrue(mLog.toString().contains("t1 exception;\n"));
+ verifyTransactionsFinished(t1);
}
@SmallTest
@Test
public void testTransactionResultException()
throws ExecutionException, InterruptedException, TimeoutException {
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
TestVoipCallTransaction.SUCCESS);
- VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+ TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
TestVoipCallTransaction.SUCCESS);
OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
new OutcomeReceiver<>() {
@@ -335,5 +379,13 @@
assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
assertEquals(expectedLog, mLog.toString());
+ verifyTransactionsFinished(t1, t2, t3);
+ }
+
+ public void verifyTransactionsFinished(TestVoipCallTransaction... transactions) {
+ for (TestVoipCallTransaction t : transactions) {
+ assertTrue("TestVoipCallTransaction[" + t.mName + "] never called finishTransaction",
+ t.isFinished);
+ }
}
}