[automerger skipped] Unbind CS if connection is not created within 15 seconds. am: 69a816bb5a -s ours am: b32da7584b -s ours am: eade481ed7 -s ours am: ca9186bc41 -s ours am: d873b88838 -s ours
am skip reason: skipped by grantmenke
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/30258347
Change-Id: I9ad2c09dc2c0aaeb4de95f98173a286138459efe
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 04b3719..65e4402 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,36 +1,37 @@
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
genrule {
name: "statslog-telecom-java-gen",
tools: ["stats-log-api-gen"],
- cmd: "$(location stats-log-api-gen) --java $(out) --module telecom"
- + " --javaPackage com.android.server.telecom --javaClass TelecomStatsLog",
+ cmd: "$(location stats-log-api-gen) --java $(out) --module telecom" +
+ " --javaPackage com.android.server.telecom --javaClass TelecomStatsLog",
out: ["com/android/server/telecom/TelecomStatsLog.java"],
}
filegroup {
- name: "Telecom-srcs",
+ name: "telecom-shell-commands-src",
+ srcs: [
+ "src/com/android/server/telecom/TelecomShellCommand.java",
+ ],
+ path: "src",
+}
+
+android_library {
+ name: "TelecomLib",
+ manifest: "AndroidManifestLib.xml",
srcs: [
"src/**/*.java",
":statslog-telecom-java-gen",
- ],
-}
-
-// Build the Telecom service.
-android_app {
- name: "Telecom",
- srcs: [
- ":Telecom-srcs",
"proto/**/*.proto",
],
static_libs: [
"androidx.annotation_annotation",
"androidx.core_core",
- ],
- libs: [
- "services",
+ "telecom_flags_core_java_lib",
+ "modules-utils-handlerexecutor",
],
resource_dirs: ["res"],
proto: {
@@ -39,6 +40,21 @@
output_params: ["optional_field_style=accessors"],
},
platform_apis: true,
+}
+
+// Build the Telecom service.
+android_app {
+ name: "Telecom",
+ srcs: [
+ ],
+ static_libs: [
+ "TelecomLib",
+ ],
+ libs: [
+ "services",
+ ],
+ resource_dirs: [],
+ platform_apis: true,
certificate: "platform",
privileged: true,
optimize: {
@@ -49,36 +65,29 @@
android_test {
name: "TelecomUnitTests",
static_libs: [
+ "TelecomLib",
"android-ex-camera2",
+ "flag-junit",
"guava",
"mockito-target-extended",
"androidx.test.rules",
"platform-test-annotations",
"androidx.legacy_legacy-support-core-ui",
"androidx.legacy_legacy-support-core-utils",
- "androidx.core_core",
"androidx.fragment_fragment",
"androidx.test.ext.junit",
"platform-compat-test-rules",
],
srcs: [
"tests/src/**/*.java",
- ":Telecom-srcs",
- "proto/**/*.proto",
],
- proto: {
- type: "nano",
- local_include_dirs: ["proto/"],
- output_params: ["optional_field_style=accessors"],
- },
resource_dirs: [
"tests/res",
- "res",
],
libs: [
- "android.test.mock",
- "android.test.base",
- "android.test.runner",
+ "android.test.mock.stubs.system",
+ "android.test.base.stubs.system",
+ "android.test.runner.stubs.system",
],
jni_libs: [
@@ -86,11 +95,6 @@
"libstaticjvmtiagent",
],
- aaptflags: [
- "--auto-add-overlay",
- "--extra-packages",
- "com.android.server.telecom",
- ],
manifest: "tests/AndroidManifest.xml",
optimize: {
enabled: false,
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ab067d9..08521a5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,7 +28,6 @@
<!-- Prevents the activity manager from delaying any activity-start
requests by this package, including requests immediately after
the user presses "home". -->
- <uses-permission android:name="android.permission.BIND_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.BIND_INCALL_SERVICE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
@@ -46,6 +45,8 @@
<!-- Required to determine source of ongoing audio recordings. -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING"/>
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+ <!-- Required to query the audio framework to determine if a notification sound should play. -->
+ <uses-permission android:name="android.permission.QUERY_AUDIO_STATE"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<!-- Required to check for direct to voicemail, to load custom ringtones for incoming calls
which are specified on a per contact basis, and also to determine user preferred
@@ -65,7 +66,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.USE_COLORIZED_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
- <uses-permission android:name="com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
+ <uses-permission android:name="android.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
<permission android:name="android.permission.BROADCAST_CALLLOG_INFO"
@@ -136,7 +137,7 @@
contacts provider entries. Any data not fitting the schema described is ignored. -->
<activity android:name=".components.UserCallActivity"
android:label="@string/userCallActivityLabel"
- android:theme="@style/Theme.Telecomm.Transparent"
+ android:theme="@style/Theme.Telecomm.UserCallActivityNoSplash"
android:permission="android.permission.CALL_PHONE"
android:excludeFromRecents="true"
android:process=":ui"
@@ -319,18 +320,6 @@
android:exported="false"
android:process=":ui"/>
- <service android:name=".components.BluetoothPhoneService"
- android:singleUser="true"
- android:process="system"
- android:exported="true">
- <intent-filter>
- <action android:name="android.bluetooth.IBluetoothHeadsetPhone"/>
- </intent-filter>
- <intent-filter>
- <action android:name="android.bluetooth.IBluetoothLeCallControlCallback" />
- </intent-filter>
- </service>
-
<service android:name=".components.TelecomService"
android:singleUser="true"
android:process="system"
diff --git a/AndroidManifestLib.xml b/AndroidManifestLib.xml
new file mode 100644
index 0000000..9b40f6b
--- /dev/null
+++ b/AndroidManifestLib.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest package="com.android.server.telecom">
+</manifest>
diff --git a/OWNERS b/OWNERS
index 97cc81f..9c071f9 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,8 +1,8 @@
+# Bug component: 151185
breadley@google.com
tgunn@google.com
-xiaotonj@google.com
-chinmayd@google.com
tjstuart@google.com
rgreenwalt@google.com
pmadapurmath@google.com
grantmenke@google.com
+huiwang@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index acab8ef..09ebfe2 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -58,5 +58,15 @@
}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsTelecomCujTestCases",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
]
}
diff --git a/flags/Android.bp b/flags/Android.bp
new file mode 100644
index 0000000..54b1443
--- /dev/null
+++ b/flags/Android.bp
@@ -0,0 +1,50 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aconfig_declarations {
+ name: "telecom_flags",
+ package: "com.android.server.telecom.flags",
+ container: "system",
+ srcs: [
+ "telecom_broadcast_flags.aconfig",
+ "telecom_ringer_flag_declarations.aconfig",
+ "telecom_api_flags.aconfig",
+ "telecom_call_filtering_flags.aconfig",
+ "telecom_incallservice_flags.aconfig",
+ "telecom_default_phone_account_flags.aconfig",
+ "telecom_callaudioroutestatemachine_flags.aconfig",
+ "telecom_call_flags.aconfig",
+ "telecom_calls_manager_flags.aconfig",
+ "telecom_anomaly_report_flags.aconfig",
+ "telecom_callaudiomodestatemachine_flags.aconfig",
+ "telecom_calllog_flags.aconfig",
+ "telecom_resolve_hidden_dependencies.aconfig",
+ "telecom_bluetoothroutemanager_flags.aconfig",
+ "telecom_work_profile_flags.aconfig",
+ "telecom_connection_service_wrapper_flags.aconfig",
+ "telecom_remote_connection_service.aconfig",
+ "telecom_profile_user_flags.aconfig",
+ "telecom_bluetoothdevicemanager_flags.aconfig",
+ "telecom_non_critical_security_flags.aconfig",
+ "telecom_headless_system_user_mode.aconfig",
+ "telecom_session_flags.aconfig",
+ "telecom_metrics_flags.aconfig",
+ ],
+}
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
new file mode 100644
index 0000000..5d42b86
--- /dev/null
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -0,0 +1,29 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+ name: "gen_anom_report_on_focus_timeout"
+ namespace: "telecom"
+ description: "When getCurrentFocusCall times out, generate an anom. report"
+ bug: "309541253"
+}
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+ name: "disconnect_self_managed_stuck_startup_calls"
+ namespace: "telecom"
+ description: "If a self-managed call is stuck in certain states, disconnect it"
+ bug: "360298368"
+}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+ name: "dont_timeout_destroyed_calls"
+ namespace: "telecom"
+ description: "When create connection timeout is hit, if call is already destroyed, skip anomaly"
+ bug: "381684580"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
new file mode 100644
index 0000000..2dfd878
--- /dev/null
+++ b/flags/telecom_api_flags.aconfig
@@ -0,0 +1,84 @@
+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"
+}
+
+# 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"
+}
+
+# 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"
+}
+
+# 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"
+}
+
+# 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"
+}
+
+# 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
+}
+
+# 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"
+}
+
+# OWNER=grantmenke TARGET=25Q2
+flag {
+ name: "allow_system_apps_resolve_voip_calls"
+ is_exported: true
+ namespace: "telecom"
+ description: "Allow system apps such as accessibility to accept and end VOIP calls."
+ bug: "353579043"
+}
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
new file mode 100644
index 0000000..3757c72
--- /dev/null
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -0,0 +1,17 @@
+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"
+}
+# OWNER=huiwang TARGET=25Q1
+flag {
+ name: "keep_bt_devices_cache_updated"
+ namespace: "telecom"
+ description: "Fix the devices cache issue of BluetoothDeviceManager"
+ bug: "380320985"
+}
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
new file mode 100644
index 0000000..dc69dd5
--- /dev/null
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -0,0 +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
new file mode 100644
index 0000000..8314376
--- /dev/null
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q3
+flag {
+ name: "is_new_outgoing_call_broadcast_unblocking"
+ namespace: "telecom"
+ description: "When set, the ACTION_NEW_OUTGOING_CALL broadcast is unblocking."
+ bug: "224550864"
+}
\ No newline at end of file
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
new file mode 100644
index 0000000..693d727
--- /dev/null
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -0,0 +1,21 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=qingzhong TARGET=24Q2
+flag {
+ name: "skip_filter_phone_account_perform_dnd_filter"
+ namespace: "telecom"
+ description: "Gates whether to still perform Dnd filter when phone account has skip_filter call extra."
+ bug: "222333869"
+}
+
+# OWNER=tjstuart TARGET=25Q1
+flag {
+ name: "check_completed_filters_on_timeout"
+ namespace: "telecom"
+ description: "If the Filtering Graph times out, combine the finished results"
+ bug: "364946812"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
new file mode 100644
index 0000000..634d7a3
--- /dev/null
+++ b/flags/telecom_call_flags.aconfig
@@ -0,0 +1,68 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+ name: "prevent_redundant_location_permission_grant_and_revoke"
+ namespace: "telecom"
+ description: "avoid redundant action of grant and revoke location permission for multiple emergency calls"
+ bug: "345386002"
+}
+
+flag {
+ name: "transactional_cs_verifier"
+ namespace: "telecom"
+ description: "verify connection service callbacks via a transaction"
+ bug: "309541257"
+}
+
+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=24Q4
+flag {
+ name: "cache_call_events"
+ namespace: "telecom"
+ description: "Cache call events to wait for the ServiceWrapper to be set"
+ bug: "364311190"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+# 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
+ }
+}
+
+# OWNER=breadley TARGET=24Q4
+flag {
+ name: "use_stream_voice_call_tones"
+ namespace: "telecom"
+ description: "Use STREAM_VOICE_CALL only for ToneGenerator"
+ bug: "363262590"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+# OWNER=tjstuart TARGET=25Q1
+flag {
+ name: "remap_transactional_capabilities"
+ namespace: "telecom"
+ description: "Transactional call capabilities need to be remapped to Connection capabilities"
+ bug: "366063695"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
new file mode 100644
index 0000000..63761ec
--- /dev/null
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "set_audio_mode_before_abandon_focus"
+ namespace: "telecom"
+ description: "Set audio mode to MODE_NORMAL before abandon the audio focus."
+ bug: "281841785"
+}
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
new file mode 100644
index 0000000..a60c0f1
--- /dev/null
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -0,0 +1,131 @@
+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"
+ description: "Fix supported routes wrongly include bluetooth issue."
+ bug: "292599751"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "use_refactored_audio_route_switching"
+ namespace: "telecom"
+ description: "Refactored audio routing"
+ bug: "306395598"
+}
+
+# OWNER=pmadapurmath TARGET=25Q1
+flag {
+ name: "resolve_active_bt_routing_and_bt_timing_issue"
+ namespace: "telecom"
+ description: "Resolve the active BT device routing and flaky timing issues noted in BT routing."
+ bug: "372029371"
+}
+
+# OWNER=tgunn TARGET=24Q3
+flag {
+ name: "ensure_audio_mode_updates_on_foreground_call_change"
+ namespace: "telecom"
+ description: "Ensure that the audio mode is updated anytime the foreground call changes."
+ bug: "289861657"
+}
+
+# OWNER=pmadapurmath TARGET=24Q1
+flag {
+ name: "ignore_auto_route_to_watch_device"
+ namespace: "telecom"
+ description: "Ignore auto routing to wearable devices."
+ bug: "294378768"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "transit_route_before_audio_disconnect_bt"
+ namespace: "telecom"
+ description: "Fix audio route transition issue on call disconnection when bt audio connected."
+ bug: "306113816"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "call_audio_communication_device_refactor"
+ namespace: "telecom"
+ description: "Refactor call audio set/clear communication device and include unsupported routes."
+ bug: "308968392"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "communication_device_protected_by_lock"
+ namespace: "telecom"
+ description: "Protect set/clear communication device operation with lock to avoid race condition."
+ bug: "303001133"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "reset_mute_when_entering_quiescent_bt_route"
+ namespace: "telecom"
+ description: "Reset mute state when entering quiescent bluetooth route."
+ bug: "311313250"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "update_route_mask_when_bt_connected"
+ namespace: "telecom"
+ description: "Update supported route mask when Bluetooth devices audio connected."
+ 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
+ }
+}
+
+# OWNER=pmadapurmath TARGET=25Q1
+flag {
+ name: "new_audio_path_speaker_broadcast_and_unfocused_routing"
+ namespace: "telecom"
+ description: "Replace the speaker broadcasts with the communication device changed listener and resolve baseline routing issues when a call ends."
+ bug: "353419513"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+# OWNER=pmadapurmath TARGET=25Q2
+flag {
+ name: "fix_user_request_baseline_route_video_call"
+ namespace: "telecom"
+ description: "Ensure that audio is routed out of speaker in a video call when we receive USER_SWITCH_BASELINE_ROUTE."
+ bug: "374037591"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
new file mode 100644
index 0000000..c0eebf1
--- /dev/null
+++ b/flags/telecom_calllog_flags.aconfig
@@ -0,0 +1,18 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=qingzhong TARGET=24Q2
+flag {
+ name: "telecom_log_external_wearable_calls"
+ namespace: "telecom"
+ description: "log external call if current device is a wearable one"
+ bug: "292600751"
+}
+
+# OWNER=ranamouawi TARGET=24Q2
+flag {
+ name: "telecom_skip_log_based_on_extra"
+ namespace: "telecom"
+ description: "skipping logging a call based on passed extra"
+ bug: "295530944"
+}
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
new file mode 100644
index 0000000..f46e844
--- /dev/null
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -0,0 +1,37 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "use_improved_listener_order"
+ namespace: "telecom"
+ description: "Make InCallController the first listener to trigger"
+ bug: "24244713"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+ name: "fix_audio_flicker_for_outgoing_calls"
+ namespace: "telecom"
+ description: "This fix ensures the MO calls won't switch from Active to Quite b/c setDialing was not called"
+ bug: "309540769"
+}
+
+# OWNER=breadley TARGET=24Q3
+flag {
+ name: "enable_call_sequencing"
+ namespace: "telecom"
+ description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
+ 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
new file mode 100644
index 0000000..8e77af5
--- /dev/null
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -0,0 +1,21 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=grantmenke TARGET=24Q2
+flag {
+ name: "updated_rcs_call_count_tracking"
+ namespace: "telecom"
+ description: "Ensure that the associatedCallCount of CS and RCS is accurately being tracked."
+ bug: "286154316"
+}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+ name: "csw_service_interface_is_null"
+ namespace: "telecom"
+ description: "fix potential NPE in onCreateConnection when the ServiceInterface is cleared out"
+ bug: "364811868"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
new file mode 100644
index 0000000..161b674
--- /dev/null
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -0,0 +1,18 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+ name: "only_update_telephony_on_valid_sub_ids"
+ namespace: "telecom"
+ description: "For testing purposes, only update Telephony when the default calling subId is non-zero"
+ bug: "234846282"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+ name: "telephony_has_default_but_telecom_does_not"
+ namespace: "telecom"
+ description: "Telecom is requesting the user to select a sim account to place the outgoing call on but the user has a default account in the settings"
+ bug: "302397094"
+}
\ No newline at end of file
diff --git a/flags/telecom_headless_system_user_mode.aconfig b/flags/telecom_headless_system_user_mode.aconfig
new file mode 100644
index 0000000..4135794
--- /dev/null
+++ b/flags/telecom_headless_system_user_mode.aconfig
@@ -0,0 +1,38 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=grantmenke TARGET=25Q1
+flag {
+ name: "telecom_main_user_in_get_respond_message_app"
+ is_exported: true
+ namespace: "telecom"
+ description: "Support HSUM mode by using the main user when getting respond via message app."
+ bug: "358587742"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+# OWNER=grantmenke TARGET=25Q1
+flag {
+ name: "telecom_main_user_in_block_check"
+ is_exported: true
+ namespace: "telecom"
+ description: "Support HSUM mode by using the main user when checking if a number is blocked."
+ bug: "369062239"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+# OWNER=grantmenke TARGET=25Q2
+flag {
+ name: "telecom_app_label_proxy_hsum_aware"
+ is_exported: true
+ namespace: "telecom"
+ description: "Support HSUM mode by ensuring AppLableProxy is multiuser aware."
+ bug: "321817633"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
new file mode 100644
index 0000000..c95816a
--- /dev/null
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -0,0 +1,42 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=qingzhong TARGET=24Q2
+flag {
+ name: "early_binding_to_incall_service"
+ namespace: "telecom"
+ description: "Binds to InCallServices when call requires no call filtering on watch"
+ bug: "282113261"
+}
+
+# OWNER=pmadapurmath TARGET=24Q2
+flag {
+ name: "ecc_keyguard"
+ namespace: "telecom"
+ description: "Ensure that users are able to return to call from keyguard UI for ECC"
+ bug: "306582821"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "separately_bind_to_bt_incall_service"
+ namespace: "telecom"
+ description: "Binding/Unbinding to BluetoothInCallServices in proper time to improve call audio"
+ bug: "306395598"
+}
+
+# OWNER=pmadapurmath TARGET=24Q4
+flag {
+ name: "on_call_endpoint_changed_ics_on_connected"
+ namespace: "telecom"
+ description: "Ensure onCallEndpointChanged is sent to ICS when it connects."
+ bug: "348297436"
+}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+ name: "do_not_send_call_to_null_ics"
+ namespace: "telecom"
+ description: "Only send calls to the InCallService if the binding is not null"
+ bug: "345473659"
+}
diff --git a/flags/telecom_metrics_flags.aconfig b/flags/telecom_metrics_flags.aconfig
new file mode 100644
index 0000000..e582e9e
--- /dev/null
+++ b/flags/telecom_metrics_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=huiwang TARGET=25Q1
+flag {
+ name: "telecom_metrics_support"
+ namespace: "telecom"
+ description: "Support telecom metrics"
+ bug: "362394177"
+}
diff --git a/flags/telecom_non_critical_security_flags.aconfig b/flags/telecom_non_critical_security_flags.aconfig
new file mode 100644
index 0000000..e492073
--- /dev/null
+++ b/flags/telecom_non_critical_security_flags.aconfig
@@ -0,0 +1,21 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+ name: "unregister_unresolvable_accounts"
+ namespace: "telecom"
+ description: "When set, Telecom will unregister accounts if the service is not resolvable"
+ bug: "281061708"
+}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+ name: "enforce_transactional_exclusivity"
+ namespace: "telecom"
+ description: "When set, ensure that transactional accounts cannot also be call capable"
+ bug: "376936125"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/flags/telecom_profile_user_flags.aconfig b/flags/telecom_profile_user_flags.aconfig
new file mode 100644
index 0000000..feee07d
--- /dev/null
+++ b/flags/telecom_profile_user_flags.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=huiwang TARGET=24Q3
+flag {
+ name: "profile_user_support"
+ namespace: "telecom"
+ description: "Fix issues related to the profile user like private profile"
+ bug: "326270861"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
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
new file mode 100644
index 0000000..e5bb1fb
--- /dev/null
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -0,0 +1,20 @@
+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
+ is_exported: true
+}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
new file mode 100644
index 0000000..6517e0f
--- /dev/null
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -0,0 +1,18 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=yeabkal TARGET=24Q2
+flag {
+ name: "use_device_provided_serialized_ringer_vibration"
+ namespace: "telecom"
+ description: "Gates whether to use a serialized, device-specific ring vibration."
+ bug: "282113261"
+}
+
+# OWNER=grantmenke TARGET=24Q4
+flag {
+ name: "ensure_in_car_ringing"
+ namespace: "telecom"
+ description: "Gates whether to ensure that when a user is in their car, they are able to hear ringing for an incoming call."
+ bug: "348708398"
+}
\ No newline at end of file
diff --git a/flags/telecom_session_flags.aconfig b/flags/telecom_session_flags.aconfig
new file mode 100644
index 0000000..5b8075c
--- /dev/null
+++ b/flags/telecom_session_flags.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=breadley TARGET=25Q1
+flag {
+ name: "end_session_improvements"
+ namespace: "telecom"
+ description: "Ensure that ending a session doesnt cause a stack overflow"
+ bug: "370349160"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
new file mode 100644
index 0000000..1891423
--- /dev/null
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+ name: "associated_user_refactor_for_work_profile"
+ namespace: "telecom"
+ description: "Redefines the associated user for calls in the context of work profile support (U+)"
+ bug: "315035693"
+}
\ No newline at end of file
diff --git a/proguard.flags b/proguard.flags
index 635eba6..7c71a15 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -9,17 +9,3 @@
-keep class android.telecom.Log {
*;
}
-
-# Keep classes, annotations and members used by Lifecycle. Remove this once aapt2 is enabled
--keepattributes *Annotation*
-
--keep class * implements android.arch.lifecycle.LifecycleObserver {
-}
-
--keep class * implements android.arch.lifecycle.GeneratedAdapter {
- <init>(...);
-}
-
--keepclassmembers class ** {
- @android.arch.lifecycle.OnLifecycleEvent *;
-}
diff --git a/proto/pulled_atoms.proto b/proto/pulled_atoms.proto
new file mode 100644
index 0000000..6c9af46
--- /dev/null
+++ b/proto/pulled_atoms.proto
@@ -0,0 +1,114 @@
+syntax = "proto2";
+
+package com.android.server.telecom;
+
+option java_package = "com.android.server.telecom";
+option java_outer_classname = "PulledAtomsClass";
+
+message PulledAtoms {
+ repeated CallStats call_stats = 1;
+ optional int64 call_stats_pull_timestamp_millis = 2;
+ repeated CallAudioRouteStats call_audio_route_stats = 3;
+ optional int64 call_audio_route_stats_pull_timestamp_millis = 4;
+ repeated TelecomApiStats telecom_api_stats = 5;
+ optional int64 telecom_api_stats_pull_timestamp_millis = 6;
+ repeated TelecomErrorStats telecom_error_stats = 7;
+ optional int64 telecom_error_stats_pull_timestamp_millis = 8;
+}
+
+/**
+ * Pulled atom to capture stats of the calls
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message CallStats {
+ // The value should be converted to android.telecom.CallDirectionEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 call_direction = 1;
+
+ // True if call is external. External calls are calls on connected Wear
+ // devices but show up in Telecom so the user can pull them onto the device.
+ optional bool external_call = 2;
+
+ // True if call is emergency call.
+ optional bool emergency_call = 3;
+
+ // True if there are multiple audio routes available
+ optional bool multiple_audio_available = 4;
+
+ // The value should be converted to android.telecom.AccountTypeEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 account_type = 5;
+
+ // UID of the package to init the call. This should always be -1/unknown for
+ // the private space calls
+ optional int32 uid = 6;
+
+ // Total number of the calls
+ optional int32 count = 7;
+
+ // Average elapsed time between CALL_STATE_ACTIVE to CALL_STATE_DISCONNECTED.
+ optional int32 average_duration_ms = 8;
+}
+
+/**
+ * Pulled atom to capture stats of the call audio route
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message CallAudioRouteStats {
+ // The value should be converted to android.telecom.CallAudioEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 call_audio_route_source = 1;
+
+ // The value should be converted to android.telecom.CallAudioEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 call_audio_route_dest = 2;
+
+ // True if the route is successful.
+ optional bool success = 3;
+
+ // True if the route is revert
+ optional bool revert = 4;
+
+ // Total number of the audio route
+ optional int32 count = 5;
+
+ // Average time from the audio route start to complete
+ optional int32 average_latency_ms = 6;
+}
+
+/**
+ * Pulled atom to capture stats of Telecom API usage
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message TelecomApiStats {
+ // The value should be converted to android.telecom.ApiNameEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 api_name = 1;
+
+ // UID of the caller. This is always -1/unknown for the private space.
+ optional int32 uid = 2;
+
+ // The value should be converted to android.telecom.ApiResultEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 api_result = 3;
+
+ // The number of times this event occurs
+ optional int32 count = 4;
+}
+
+/**
+ * Pulled atom to capture stats of Telecom module errors
+ * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
+ */
+message TelecomErrorStats {
+ // The value should be converted to android.telecom.SubmoduleEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 submodule = 1;
+
+ // The value should be converted to android.telecom.ErrorEnum
+ // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+ optional int32 error = 2;
+
+ // The number of times this error occurs
+ optional int32 count = 3;
+}
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 50bead5..71564e8 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"As jy antwoord, sal dit jou huidige video-oproep beëindig"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Antwoord"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Wys af"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Oproep kan nie geplaas word nie, want daar is geen oproeprekeninge wat hierdie tipe oproepe ondersteun nie."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kan nie oproep maak nie. Gaan jou toestel se verbinding na."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Oproep kan nie gemaak word nie weens jou <xliff:g id="OTHER_CALL">%1$s</xliff:g>-oproep."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Oproep kan nie gemaak word nie weens jou <xliff:g id="OTHER_CALL">%1$s</xliff:g>-oproepe."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Oproep kan nie gemaak word nie weens \'n oproep in \'n ander program."</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index f0923d5..dafbe6e 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"መመለስ እየተካሄደ ያለ የቪዲዮ ጥሪዎን ይጨርሳል"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ይመልሱ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"አትቀበል"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"የዚህን ዓይነት ጥሪዎች የሚደግፉ መደወያ መለያዎች ስለሌሉ ጥሪ መደረግ አይችልም።"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ጥሪ ማድረግ አልተቻለም። የመሣሪያዎን ግንኙነት ይፈትሹ።"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"በ<xliff:g id="OTHER_CALL">%1$s</xliff:g> ጥሪዎ ምክንያት ጥሪ መደረግ አይችልም።"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"በ<xliff:g id="OTHER_CALL">%1$s</xliff:g> ጥሪዎችዎ ምክንያት ጥሪዎች መደረግ አይችሉም።"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"በሌላ መተግበሪያ ውስጥ ባለ ጥሪ ምክንያት ጥሪ መደረግ አይችልም።"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 2a56809..9eb3a35 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"سيؤدي الرد إلى إنهاء مكالمات الفيديو"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"رد"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"رفض"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"يتعذَّر إجراء المكالمة بسبب عدم وجود حسابات اتصال يمكن استخدامها مع المكالمات من هذا النوع."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"لا يمكن إجراء المكالمة. يُرجى التأكّد من إمكانية الاتصال على جهازك."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"يتعذر إجراء المكالمة نتيجة لمكالمة <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"يتعذر إجراء المكالمة نتيجة لمكالمات <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"يتعذر إجراء المكالمة نتيجة لوجود مكالمة في تطبيق آخر."</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 72ac4db..668f5e5 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"উত্তৰ দিলে আপোনাৰ বৰ্তমান চলি থকা ভিডিঅ\' কলটোৰ অন্ত পৰিব"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"উত্তৰ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"প্ৰত্যাখ্যান কৰক"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"এইধৰণৰ কল কৰিব পৰা কলিং একাউণ্ট নোহোৱাৰ কাৰণে কল কৰিব নোৱাৰি।"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"কল কৰিব নোৱাৰি। আপোনাৰ ডিভাইচৰ সংযোগ পৰীক্ষা কৰক।"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"আপোনাৰ <xliff:g id="OTHER_CALL">%1$s</xliff:g> কল চলি থকাৰ কাৰণে বেলেগ কল কৰিব নোৱাৰি।"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"আপোনাৰ <xliff:g id="OTHER_CALL">%1$s</xliff:g> কলকেইটা চলি থকাৰ কাৰণে বেলেগ কল কৰিব নোৱাৰি।"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"অইন এটা এপত কল চলি থকাৰ কাৰণে বেলেগ কল কৰিব নোৱাৰি।"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index ead7f54..c975159 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Cavab versəniz, davam edən video zəng sonlandırılacaq"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Cavab"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rədd edin"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Bu növ zəngləri dəstəkləyən hesablar olmadığına görə zəng etmək mümkün deyil."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Zəng etmək olmur. Cihazınızın bağlantısını yoxlayın."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> zəngi səbəbilə çağrı edilə bilməz."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> zəngləri səbəbilə çağrı edilə bilməz."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Başqa bir tətbiqdəki zəng səbəbilə çağrı edilə bilməz."</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index d527842..3709c25 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ako odgovorite, završićete video poziv koji je u toku"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Odgovori"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Odbij"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Upućivanje poziva nije moguće jer nemate nijedan nalog za pozivanje koji podržava pozive ovog tipa."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Pozivanje nije uspelo. Proverite vezu uređaja."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ne možete da uputite poziv zbog <xliff:g id="OTHER_CALL">%1$s</xliff:g> poziva."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ne možete da uputite poziv zbog <xliff:g id="OTHER_CALL">%1$s</xliff:g> poziva."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ne možete da uputite poziv zbog poziva u drugoj aplikaciji."</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index c5b59bd..c3c6e2f 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Адказ на гэты выклік завершыць ваш бягучы відэавыклік"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Адказаць"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Адхіліць"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Не ўдалося зрабіць выклік, бо на прыладзе няма ўліковых запісаў для гэтага тыпу выклікаў."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не ўдаецца зрабіць выклік. Праверце падключэнне прылады."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Выклік немагчыма выканаць, бо ідзе выклік <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Выклік немагчыма выканаць, бо ідуць выклікі <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Выклік немагчыма выканаць, бо ідзе выклік у іншай праграме."</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index fe5d70f..116c884 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ако отговорите, текущото ви видеообаждане ще прекъсне"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Отговаряне"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Отхвърляне"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Обаждането не може да бъде извършено, защото няма профили за обаждане, които поддържат обаждания от този тип."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не може да се извърши обаждане. Проверете връзката на устройството си."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Не можете да се обадите заради обаждането си през <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Не можете да се обадите заради обажданията си през <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Не можете да се обадите заради обаждане в друго приложение."</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 49e6ba3..4f4fea6 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"উত্তর দেওয়া হলে আপনার চালু থাকা ভিডিও কলটি কেটে যাবে"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"উত্তর দিন"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"প্রত্যাখ্যান করুন"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"এই ধরনের কল করার জন্য যে কলিং অ্যাকাউন্টের প্রয়োজন সেটি না থাকার জন্য এই কলটি করা যাবে না।"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"কল করা যাবে না। আপনার ডিভাইসের কানেকশন চেক করুন।"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"আপনার <xliff:g id="OTHER_CALL">%1$s</xliff:g> কলটির কারণে কলটি করা যাবে না।"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"আপনার <xliff:g id="OTHER_CALL">%1$s</xliff:g> কলগুলির কারণে কলটি করা যাবে না।"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"অন্য একটি অ্যাপের কলের কারণে কলটি করা যাবে না।"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 61b86db..ba75d0c 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Odgovaranje će prekinuti video poziv koji je u toku"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Odgovori"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Odbij"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ne može se uputiti poziv zato što ne postoji nijedan račun za pozivanje koji podržava ovu vrstu poziva."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nije moguće uputiti poziv. Provjerite vezu uređaja."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Pozivanje nije moguće zbog poziva: <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Pozivanje nije moguće zbog poziva: <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Pozivanje nije moguće zbog poziva u drugoj aplikaciji."</string>
@@ -100,7 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Pozivi u pozadini"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Prekinuti pozivi"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Padovi aplikacija za telefon"</string>
- <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Prijenos poziva"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Prenos poziva"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Upućivanje ovog poziva će prekinuti poziv: <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Odaberite kako želite uputiti ovaj poziv"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Preusmjeri poziv pomoću aplikacije <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -131,7 +131,7 @@
<string name="callendpoint_name_speaker" msgid="1971760468695323189">"Zvučnik"</string>
<string name="callendpoint_name_streaming" msgid="2337595450408275576">"Vanjski"</string>
<string name="callendpoint_name_unknown" msgid="2199074708477193852">"Nepoznato"</string>
- <string name="call_streaming_notification_body" msgid="502216105683378263">"Prijenos zvuka na drugom uređaju"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Prenos zvuka na drugom uređaju"</string>
<string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini vezu"</string>
<string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Prebaci ovdje"</string>
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 113d144..5793449 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"En respondre, finalitzarà la videotrucada en curs"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Respon"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rebutja"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"No es pot trucar perquè, en aquest moment, no hi ha cap compte de trucades compatible amb les trucades d\'aquest tipus."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"No es pot fer la trucada. Comprova la connexió del dispositiu."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"No es pot trucar perquè ja hi ha una trucada en curs a <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"No es pot trucar perquè ja hi ha trucades en curs a <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"No es pot trucar perquè ja hi ha una trucada en curs en una altra aplicació."</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index ab74d61..0619308 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -36,7 +36,7 @@
<string name="accessibility_call_muted" msgid="2968461092554300779">"Hovor ztlumen."</string>
<string name="accessibility_speakerphone_enabled" msgid="555386652061614267">"Reproduktor je zapnutý."</string>
<string name="respond_via_sms_canned_response_1" msgid="6332561460870382561">"Teď nemůžu mluvit, o co jde?"</string>
- <string name="respond_via_sms_canned_response_2" msgid="2052951316129952406">"Zavolám později."</string>
+ <string name="respond_via_sms_canned_response_2" msgid="2052951316129952406">"Zavolám zpátky."</string>
<string name="respond_via_sms_canned_response_3" msgid="6656147963478092035">"Zavolám později."</string>
<string name="respond_via_sms_canned_response_4" msgid="9141132488345561047">"Nemůžu telefonovat. Zavoláš později?"</string>
<string name="respond_via_sms_setting_title" msgid="4762275482898830160">"Rychlé odpovědi"</string>
@@ -63,7 +63,7 @@
<string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Nastavit jako výchozí"</string>
<string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Zrušit"</string>
<string name="blocked_numbers" msgid="8322134197039865180">"Blokovaná čísla"</string>
- <string name="blocked_numbers_msg" msgid="2797422132329662697">"Ze zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
+ <string name="blocked_numbers_msg" msgid="2797422132329662697">"Od zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
<string name="block_number" msgid="3784343046852802722">"Přidat číslo"</string>
<string name="unblock_dialog_body" msgid="2723393535797217261">"Odblokovat číslo <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
<string name="unblock_button" msgid="8732021675729981781">"Odblokovat"</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Přijetím hovoru ukončíte probíhající videohovor"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Přijmout"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Odmítnout"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Hovor není možné provést, protože není k dispozici žádný účet, který by tento typ hovoru podporoval."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nelze volat. Zkontrolujte připojení zařízení."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Hovor není možné provést kvůli hovoru <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Hovor není možné provést kvůli hovorům <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Hovor není možné provést kvůli hovoru v jiné aplikaci."</string>
@@ -113,7 +113,7 @@
<string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"Blokovat volající, kteří skrývají své číslo"</string>
<string name="phone_settings_payphone_txt" msgid="5003987966052543965">"Z veřejných telefonů"</string>
<string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"Blokovat hovory z veřejných telefonů"</string>
- <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Z nerozpoznaných čísel"</string>
+ <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Nerozpoznaná čísla"</string>
<string name="phone_settings_unknown_summary_txt" msgid="5446657192535779645">"Blokovat hovory od nerozpoznaných volajících"</string>
<string name="phone_settings_unavailable_txt" msgid="825918186053980858">"Neznámé"</string>
<string name="phone_settings_unavailable_summary_txt" msgid="8221686031038282633">"Blokovat hovory z neznámých čísel"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 4eead66..0eb69ff 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Hvis du besvarer, afsluttes dit igangværende videoopkald"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Besvar"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Afvis"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Opkaldet kan ikke foretages, fordi der ikke er nogen opkaldskonti, der understøtter opkald af denne type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Det er ikke muligt at foretage opkald. Tjek din enheds forbindelse."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Opkaldet kan ikke foretages på grund af dit opkald i <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Opkaldet kan ikke foretages på grund af dine opkald i <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Opkaldet kan ikke foretages på grund et opkald i en anden app."</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index dccdb87..665124a 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Wenn du den Anruf annimmst, wird der Videoanruf beendet"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Annehmen"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Ablehnen"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Der Anruf kann nicht ausgehen, da es keine Anrufkonten gibt, die Anrufe dieses Typs unterstützen."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Keine Anrufe möglich. Prüfe die Verbindung deines Geräts."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Dieser Anruf kann aufgrund des Anrufs in <xliff:g id="OTHER_CALL">%1$s</xliff:g> nicht getätigt werden."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Dieser Anruf kann aufgrund deiner Anrufe in <xliff:g id="OTHER_CALL">%1$s</xliff:g> nicht getätigt werden."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Dieser Anruf kann aufgrund eines Anrufs in einer anderen App nicht getätigt werden."</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 6b58863..ba504d7 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Εάν απαντήσετε, η τρέχουσα βιντεοκλήση σας θα τερματιστεί"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Απάντηση"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Απόρριψη"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, επειδή δεν υπάρχουν λογαριασμοί κλήσεων που υποστηρίζουν κλήσεις αυτού του τύπου."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης. Ελέγξτε τη σύνδεση της συσκευής σας."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, λόγω της κλήσης σας μέσω <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, λόγω των κλήσεών σας μέσω <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, λόγω κάποιας κλήσης μέσω άλλης εφαρμογής."</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 250ab62..1ce62df 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts that support calls of this type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index e6291f4..8ae9c0a 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts which support calls of this type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 250ab62..1ce62df 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts that support calls of this type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 250ab62..1ce62df 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts that support calls of this type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 5bd0e25..6849084 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts which support calls of this type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index c0f4e17..668a696 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Si respondes, finalizará tu videollamada en curso"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Responder"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rechazar"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"No se puede realizar la llamada porque no hay ninguna cuenta compatible con este tipo de llamadas."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"No se puede realizar la llamada. Comprueba la conexión del dispositivo."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"No se puede realizar la llamada porque hay una llamada en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"No se puede realizar la llamada porque hay otras llamadas en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"No se puede realizar la llamada porque hay una llamada en curso en otra app."</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 20b80a5..96163b3 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Al responder, finalizará la videollamada en curso"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Responder"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rechazar"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"No puedes llamar porque no hay cuentas de llamada que admitan este tipo de llamadas."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"No se puede hacer la llamada. Comprueba la conexión de tu dispositivo."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"No puedes llamar porque tienes una llamada de <xliff:g id="OTHER_CALL">%1$s</xliff:g> en curso."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"No puedes llamar porque tienes varias llamadas de <xliff:g id="OTHER_CALL">%1$s</xliff:g> en curso."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"No puedes llamar porque tienes una llamada en curso en otra aplicación."</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index cac1fd6..6fd5592 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Vastamisel lõpetatakse pooleliolev videokõne"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Vasta"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Keeldu"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Helistada ei saa, kuna pole ühtegi kõnekontot, mis toetaks seda tüüpi kõnesid."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kõnet ei saa teha. Kontrollige seadme ühendust."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Kõnet ei saa teenuse <xliff:g id="OTHER_CALL">%1$s</xliff:g> kõne tõttu teha."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Kõnet ei saa teenuse <xliff:g id="OTHER_CALL">%1$s</xliff:g> kõnede tõttu teha."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Kõnet ei saa teise rakenduse kõne tõttu teha."</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index d1aa545..3efbc07 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Erantzuten baduzu, amaitu egingo da oraingo bideodeia"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Erantzun"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Baztertu"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ezin da egin deia, ez dagoelako mota honetako deiak onartzen duen deiak egiteko konturik."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Ezin da egin deia. Egiaztatu gailua konektatuta dagoela."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ezin da egin deia, beste dei bat abian delako <xliff:g id="OTHER_CALL">%1$s</xliff:g> zerbitzuan."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ezin da egin deia, beste dei batzuk abian direlako <xliff:g id="OTHER_CALL">%1$s</xliff:g> zerbitzuan."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ezin da egin deia, beste dei bat abian delako beste aplikazio batean."</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 8d562ec..6bd2ff6 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"پاسخگویی به تماس تصویری درحال انجامتان پایان میدهد"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"پاسخگویی"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"نپذیرفتن"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"بهدلیل اینکه هیچ حساب تماسی وجود ندارد که از این نوع تماس پشتیبانی کند، تماس برقرار نشد."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"تماس برقرار نشد. اتصال دستگاهتان را بررسی کنید."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"به دلیل تماس <xliff:g id="OTHER_CALL">%1$s</xliff:g>، نمیتوان تماسی برقرار کرد."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"به دلیل تماسهای <xliff:g id="OTHER_CALL">%1$s</xliff:g>، نمیتوان تماسی برقرار کرد."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"به دلیل تماسی در برنامه دیگر، نمیتوان تماسی برقرار کرد."</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 338e429..0d5fdbb 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Vastaaminen päättää käynnissä olevan videopuhelun."</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Vastaa"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Hylkää"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Puhelua ei voi soittaa, koska laitteella ei ole puhelutiliä, joka tukisi tätä puhelutyyppiä."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Soittaminen epäonnistui. Tarkista laitteen yhteys."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Puhelua ei voi soittaa, koska toisessa sovelluksessa (<xliff:g id="OTHER_CALL">%1$s</xliff:g>) on puhelu käynnissä."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Puhelua ei voi soittaa, koska toisessa sovelluksessa (<xliff:g id="OTHER_CALL">%1$s</xliff:g>) on puheluja käynnissä."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Puhelua ei voi soittaa, koska toisessa sovelluksessa on puhelu käynnissä."</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index aaf651f..cfd153b 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -30,9 +30,9 @@
<string name="notification_disconnectedCall_body" msgid="600491714584417536">"L\'appel à <xliff:g id="CALLER">%s</xliff:g> a été déconnecté en raison d\'un appel d\'urgence qui a été passé."</string>
<string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"Votre appel a été déconnecté en raison d\'un appel d\'urgence en cours de lancement."</string>
<string name="notification_audioProcessing_title" msgid="1619035039880584575">"Appel en arrière-plan"</string>
- <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> traite un appel en arrière-plan. Cette application peut accéder à l\'audio de l\'appel et faire jouer un contenu audio par l\'intermédiaire de l\'appel."</string>
+ <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> traite un appel en arrière-plan. Cette appli peut accéder à l\'audio de l\'appel et faire jouer un contenu audio par l\'intermédiaire de l\'appel."</string>
<string name="notification_incallservice_not_responding_title" msgid="5347557574288598548">"<xliff:g id="IN_CALL_SERVICE_APP_NAME">%s</xliff:g> a arrêté de répondre"</string>
- <string name="notification_incallservice_not_responding_body" msgid="9209308270131968623">"Votre appel a utilisé l\'application Téléphone intégrée à votre appareil"</string>
+ <string name="notification_incallservice_not_responding_body" msgid="9209308270131968623">"Votre appel a utilisé l\'appli Téléphone intégrée à votre appareil"</string>
<string name="accessibility_call_muted" msgid="2968461092554300779">"Son coupé"</string>
<string name="accessibility_speakerphone_enabled" msgid="555386652061614267">"Haut-parleur activé"</string>
<string name="respond_via_sms_canned_response_1" msgid="6332561460870382561">"Peux pas parler. Quoi de neuf?"</string>
@@ -47,20 +47,20 @@
<string name="respond_via_sms_failure_format" msgid="5198680980054596391">"Échec de l\'envoi du message au <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
<string name="enable_account_preference_title" msgid="6949224486748457976">"Comptes d\'appel"</string>
<string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"Seuls les appels d\'urgence sont autorisés."</string>
- <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Cette application ne peut pas faire d\'appels sans l\'autorisation de l\'application Téléphone."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Cette appli ne peut pas faire d\'appels sans l\'autorisation de l\'appli Téléphone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Pour faire un appel, entrez un numéro valide."</string>
<string name="duplicate_video_call_not_allowed" msgid="5754746140185781159">"Impossible d\'ajouter l\'appel pour le moment."</string>
<string name="no_vm_number" msgid="2179959110602180844">"Numéro de messagerie vocale manquant"</string>
<string name="no_vm_number_msg" msgid="1339245731058529388">"Aucun numéro de messagerie vocale n\'est enregistré sur la carte SIM."</string>
<string name="add_vm_number_str" msgid="5179510133063168998">"Ajouter un numéro"</string>
- <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme votre application de téléphonie par défaut?"</string>
- <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Définir comme application de téléphonie par défaut"</string>
+ <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme votre appli de téléphonie par défaut?"</string>
+ <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Définir comme appli de téléphonie par défaut"</string>
<string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"Annuler"</string>
- <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> sera en mesure de passer des appels et de contrôler tous les aspects des appels. Seules les applications auxquelles vous avez confiance doivent être définies comme l\'application de téléphonie par défaut."</string>
+ <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> sera en mesure de passer des appels et de contrôler tous les aspects des appels. Seules les applis auxquelles vous avez confiance doivent être définies comme appli de téléphonie par défaut."</string>
<string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme appli de filtrage d\'appels par défaut?"</string>
<string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ne pourra plus filtrer les appels."</string>
- <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> pourra accéder aux renseignements sur les appelants qui ne figurent pas dans vos contacts et pourra bloquer ces appels. L\'application de filtrage d\'appels par défaut doit être une application de confiance."</string>
- <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Définir comme application de filtrage d\'appels par défaut"</string>
+ <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> pourra accéder aux renseignements sur les appelants qui ne figurent pas dans vos contacts et pourra bloquer ces appels. L\'appli de filtrage d\'appels par défaut doit être une appli de confiance."</string>
+ <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Définir comme appli de filtrage d\'appels par défaut"</string>
<string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Annuler"</string>
<string name="blocked_numbers" msgid="8322134197039865180">"Numéros bloqués"</string>
<string name="blocked_numbers_msg" msgid="2797422132329662697">"Vous ne recevrez pas d\'appels ni de messages texte provenant des numéros bloqués."</string>
@@ -90,22 +90,22 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Si vous répondez, vous mettrez fin à l\'appel vidéo en cours"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Répondre"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Refuser"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossible de passer cet appel, car aucun compte d\'appel ne prend en charge les appels de ce type."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Impossible de passer l\'appel. Vérifiez la connexion de votre appareil."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Impossible de faire l\'appel en raison de votre appel <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Impossible de faire l\'appel en raison de vos appels <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
- <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossible de faire l\'appel en raison d\'un appel dans une autre application."</string>
+ <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossible de faire l\'appel en raison d\'un appel dans une autre appli."</string>
<string name="notification_channel_incoming_call" msgid="5245550964701715662">"Appels entrants"</string>
<string name="notification_channel_missed_call" msgid="7168893015283909012">"Appels manqués"</string>
<string name="notification_channel_call_blocking" msgid="2028807677868598710">"Blocage des appels"</string>
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Appels en arrière-plan"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Appels déconnectés"</string>
- <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applications téléphoniques qui ont planté"</string>
+ <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applis téléphoniques qui ont planté"</string>
<string name="notification_channel_call_streaming" msgid="5100510699787538991">"Diffusion en continu d\'appels"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Si vous passez cet appel, vous mettrez fin à l\'appel <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choisissez comment passer cet appel"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Rediriger l\'appel en utilisant <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
<string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Appeler en utilisant mon numéro de téléphone"</string>
- <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Impossible de passer l\'appel au moyen de l\'application <xliff:g id="OTHER_APP">%1$s</xliff:g>. Essayez d\'utiliser une autre application de redirection d\'appels ou de communiquer avec le développeur de l\'application pour obtenir de l\'aide."</string>
+ <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Impossible de passer l\'appel au moyen de l\'appli <xliff:g id="OTHER_APP">%1$s</xliff:g>. Essayez d\'utiliser une autre appli de redirection d\'appels ou de communiquer avec le développeur de l\'appli pour obtenir de l\'aide."</string>
<string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"Blocage des appels"</string>
<string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"Numéros non répertoriés dans les contacts"</string>
<string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"Bloquer les numéros non répertoriés dans vos contacts"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index a14cbb1..9dbca8f 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Si vous répondez, vous mettrez fin à l\'appel vidéo en cours"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Répondre"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Refuser"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossible de passer cet appel, car aucun compte téléphonique ne prend en charge ce type d\'appel."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Impossible de passer l\'appel. Vérifiez la connexion de votre appareil."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Vous ne pouvez pas passer cet appel, car vous avez une communication en cours dans <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Vous ne pouvez pas passer cet appel, car vous avez des communications en cours dans <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Vous ne pouvez pas passer cet appel, car vous avez une communication en cours dans une autre application."</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 8e82fce..f8eb32c 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ao responder, finalizarán as túas videochamadas en curso"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Contestar"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rexeitar"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Non se pode realizar a chamada porque non hai ningunha conta de chamadas que admita chamadas deste tipo."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Non se puido facer a chamada. Revisa a conexión do dispositivo."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Non se pode realizar a chamada porque hai unha chamada en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Non se pode realizar a chamada porque hai chamadas en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Non se pode realizar a chamada porque hai chamadas en curso noutra aplicación."</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 1b5c5ce..dd04bcf 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -43,8 +43,8 @@
<string name="respond_via_sms_setting_title_2" msgid="4914853536609553457">"હાજરજવાબમાં ફેરફાર કરો"</string>
<string name="respond_via_sms_setting_summary" msgid="8054571501085436868"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="6579353156073272157">"ઝડપી પ્રતિસાદ"</string>
- <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર સંદેશ મોકલ્યો."</string>
- <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"<xliff:g id="PHONE_NUMBER">%s</xliff:g>ને સંદેશ મોકલવામાં નિષ્ફળ રહ્યાં."</string>
+ <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર મેસેજ મોકલ્યો."</string>
+ <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર મેસેજ મોકલવામાં નિષ્ફળ રહ્યાં."</string>
<string name="enable_account_preference_title" msgid="6949224486748457976">"કૉલ કરવા માટેના એકાઉન્ટ"</string>
<string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"ફક્ત કટોકટીના કૉલ્સને મંજૂરી છે."</string>
<string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"ફોન પરવાનગી વિના આ ઍપ્લિકેશન આઉટગોઇંગ કૉલ્સ કરી શકતી નથી."</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"જવાબ આપવાથી તમારો ચાલુ વિડિઓ કૉલ સમાપ્ત થશે"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"જવાબ આપો"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"નકારો"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"કૉલ કરી શકાતો નથી કારણ કે આ પ્રકારના કૉલની સુવિધા આપતા હોય એવા કોઈ કૉલિંગ એકાઉન્ટ નથી."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"કૉલ કરી શકતા નથી. તમારા ડિવાઇસનું કનેક્શન ચેક કરો."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"તમારા <xliff:g id="OTHER_CALL">%1$s</xliff:g> કૉલને કારણે કૉલ કરી શકતાં નથી."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"તમારા <xliff:g id="OTHER_CALL">%1$s</xliff:g> કૉલને કારણે કૉલ કરી શકતાં નથી."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"અન્ય ઍપ્લિકેશનમાં કૉલને કારણે કૉલ કરી શકતાં નથી."</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index c32f582..683a5ab 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"उत्तर देने से आपका जारी वीडियो कॉल खत्म हो जाएगा"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"उत्तर दें"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"अस्वीकार करें"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"कॉल नहीं किया जा सकता क्योंकि कॉल करने के लिए ऐसा कोई खाता नहीं है जिस पर इस तरह के कॉल की सुविधा हो."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"कॉल नहीं किया जा सकता. अपने डिवाइस के कनेक्शन की जांच करें."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"आपके <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉल के कारण कॉल नहीं लगाया जा सकता."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"आपके <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉल के कारण कॉल नहीं लगाया जा सकता."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"किसी दूसरे ऐप्लिकेशन में कॉल के कारण कॉल नहीं लगाया जा सकता."</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index d6b209e..b664e5c 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ako odgovorite, prekinut ćete videopoziv u tijeku"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Odgovori"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Odbij"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Poziv se ne može uputiti jer nema računa za pozivanje koji podržavaju pozive te vrste."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Poziv se ne može uputiti. Provjerite vezu uređaja."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Poziv se ne može uspostaviti zbog poziva u aplikaciji <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Poziv se ne može uspostaviti zbog poziva u aplikaciji <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Poziv se ne može uspostaviti zbog poziva u drugoj aplikaciji."</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 63f04b6..0a0c377 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ha válaszol a hívásra, megszakítja a meglévő videohívást"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Hívás fogadása"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Elutasítás"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"A hívás nem indítható el, mert nincs olyan hívásra alkalmas fiók, amely támogatná az ilyen típusú hívásokat."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nem lehet hívást indítani. Ellenőrizze eszköze kapcsolatát."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"A(z) <xliff:g id="OTHER_CALL">%1$s</xliff:g>-hívás miatt nem indítható hívás."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"A(z) <xliff:g id="OTHER_CALL">%1$s</xliff:g>-hívások miatt nem indítható hívás."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Egy másik alkalmazásban folytatott hívás miatt nem indítható hívás."</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 169ea36..7f877c5 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Եթե պատասխանեք այս զանգին, ընթացիկ տեսազանգը կընդհատվի"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Պատասխանել"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Մերժել"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Զանգը հնարավոր չէ կատարել, քանի որ հաշիվներ չկան, որոնք աջակցում են այս տեսակի զանգեր:"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Հնարավոր չէ զանգել։ Ստուգեք սարքի միացումը։"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Զանգը հնարավոր չէ կատարել՝ <xliff:g id="OTHER_CALL">%1$s</xliff:g>-ի ընթացիկ զանգի պատճառով:"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Զանգը հնարավոր չէ կատարել՝ <xliff:g id="OTHER_CALL">%1$s</xliff:g>-ի ընթացիկ զանգերի պատճառով:"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Զանգը հնարավոր չէ կատարել՝ մեկ այլ հավելվածի ընթացիկ զանգի պատճառով:"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 1e51f7a..34c0c66 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Menjawab panggilan akan mengakhiri panggilan video yang sedang berlangsung"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Jawab"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Tolak"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Panggilan tidak dapat dilakukan karena tidak ada akun panggilan yang mendukung jenis panggilan ini."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Tidak dapat menelepon. Periksa koneksi perangkat Anda."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Panggilan tidak dapat dilakukan karena panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> Anda."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Panggilan tidak dapat dilakukan karena panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> Anda."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Panggilan tidak dapat dilakukan karena adanya panggilan di aplikasi lain."</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 7009b7c..c2fcf8f 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ef þessu er svarað lýkur myndsímtalinu"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Svara"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Hafna"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ekki er hægt að hringja vegna þess að engir símtalareikningar eru til staðar sem styðja svona símtöl."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Ekki er hægt að hringja. Athugaðu tengingu tækisins."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ekki er hægt að hringja sökum símtalsins með <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ekki er hægt að hringja sökum símtala með <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ekki er hægt að hringja sökum símtals í öðru forriti."</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 4a17d18..42cb0c8 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Se rispondi, la videochiamata in corso verrà terminata"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Rispondi"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rifiuta"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossibile effettuare la chiamata perché non sono presenti account che supportano chiamate di questo tipo."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Impossibile effettuare la chiamata. Controlla la connessione del dispositivo."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Impossibile effettuare la chiamata a causa della chiamata <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Impossibile effettuare la chiamata a causa delle chiamate <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossibile effettuare la chiamata a causa di una chiamata in un\'altra app."</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 05ec712..98c5347 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"מענה יסיים את שיחת הווידאו הנוכחית"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"מענה"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"דחייה"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"אי אפשר להתקשר כי אין במכשיר חשבון שתומך בשיחות מהסוג הזה."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"אי אפשר להתקשר. כדאי לבדוק את החיבורים השונים של המכשיר."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"אי אפשר להתקשר בגלל שיש שיחה ב-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"אי אפשר להתקשר בגלל שיש שיחות ב-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"אי אפשר להתקשר בגלל שיש שיחה באפליקציה אחרת."</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 19387ff..2df6736 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"応答すると、進行中のビデオ通話は終了します"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"応答"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"拒否"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"この種の通話に対応している通話アカウントがないため、通話を発信できません。"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"発信できません。デバイスの接続状態を確認してください。"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> で通話中のため、この通話を発信することはできません。"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> で通話中のため、この通話を発信することはできません。"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"別のアプリで通話中のため、この通話を発信することはできません。"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index d56873f..f2a3e90 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"პასუხის გაცემა თქვენს მიმდინარე ვიდეოზარს დაასრულებს"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"პასუხი"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"უარყოფა"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ზარის განხორციელება შეუძლებელია, რადგან არ არის დარეკვის ის ანგარიშები, რომლებიც მხარს უჭერს ამ ტიპის ზარებს."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ზარი ვერ ხორციელდება. შეამოწმეთ თქვენი მოწყობილობის კავშირი."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ზარი ვერ ხორციელდება <xliff:g id="OTHER_CALL">%1$s</xliff:g> ზარის გამო."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ზარი ვერ ხორციელდება <xliff:g id="OTHER_CALL">%1$s</xliff:g> ზარების გამო."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ზარი ვერ ხორციელდება ზარის გამო სხვა აპში."</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 399da20..22ac1fc 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -62,7 +62,7 @@
<string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> контактілер тізімінде жоқ қоңырау шалушылар туралы ақпаратты көріп, бұндай қоңырауларды бөгей алады. Әдепкі қоңырауды тексеру қолданбасы ретінде тек өзіңіз сенетін қолданбаларды ғана орнатқан дұрыс."</string>
<string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Әдепкі ретінде орнату"</string>
<string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Жабу"</string>
- <string name="blocked_numbers" msgid="8322134197039865180">"Бөгелген нөмірлер"</string>
+ <string name="blocked_numbers" msgid="8322134197039865180">"Блокталған нөмірлер"</string>
<string name="blocked_numbers_msg" msgid="2797422132329662697">"Тыйым салынған нөмірлерден қоңыраулар немесе мәтіндік хабарлар алмайсыз."</string>
<string name="block_number" msgid="3784343046852802722">"Нөмір қосу"</string>
<string name="unblock_dialog_body" msgid="2723393535797217261">"<xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g> бөгеуден шығарылсын ба?"</string>
@@ -70,7 +70,7 @@
<string name="add_blocked_dialog_body" msgid="8599974422407139255">"Қоңыраулары мен мәтіндік хабарлары бөгелетін нөмір"</string>
<string name="add_blocked_number_hint" msgid="8769422085658041097">"Телефон нөмірі"</string>
<string name="block_button" msgid="485080149164258770">"Блоктау"</string>
- <string name="non_primary_user" msgid="315564589279622098">"Бөгелген нөмірлерді тек құрылғы иесі көре және басқара алады."</string>
+ <string name="non_primary_user" msgid="315564589279622098">"Блокталған нөмірлерді тек құрылғы иесі көре және басқара алады."</string>
<string name="delete_icon_description" msgid="5335959254954774373">"Бөгеуді алу"</string>
<string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"Тыйым уақытша алынды"</string>
<string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Төтенше жағдай нөмірін терген немесе мәтіндік хабар жіберген соң, төтенше жағдай қызметтері сізге хабарласа алуы үшін тыйым алынады."</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Жауап беру қазіргі бейне қоңырауды тоқтатады"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Жауап беру"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Қабылдамау"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Қоңырау шалу мүмкін емес, себебі бұндай қоңырауларға қолдау көрсететін аккаунт жоқ."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Қоңырау шалу мүмкін емес. Құрылғы байланысын тексеріңіз."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Қоңырау шалу мүмкін емес, себебі <xliff:g id="OTHER_CALL">%1$s</xliff:g> қоңырауы белсенді."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Қоңырау шалу мүмкін емес, себебі <xliff:g id="OTHER_CALL">%1$s</xliff:g> қоңыраулары белсенді."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Қоңырау шалу мүмкін емес, себебі басқа қолданбадан қоңырау шалынуда."</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 1c28d37..41b02f3 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ការឆ្លើយនឹងបញ្ចប់ការហៅតាមវីដេអូដែលកំពុងតែដំណើរការរបស់អ្នក"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ឆ្លើយ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"បដិសេធ"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"មិនអាចធ្វើការហៅទូរសព្ទបានទេ ពីព្រោះមិនមានគណនីហៅទូរសព្ទដែលអាចប្រើបានជាមួយការហៅប្រភេទនេះទេ។"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"មិនអាចហៅទូរសព្ទបានទេ។ សូមពិនិត្យមើលការតភ្ជាប់របស់ឧបករណ៍អ្នក។"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ការហៅមិនអាចធ្វើបានទេ ដោយសារការហៅ <xliff:g id="OTHER_CALL">%1$s</xliff:g> របស់អ្នក។"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ការហៅមិនអាចធ្វើបានទេ ដោយសារការហៅ <xliff:g id="OTHER_CALL">%1$s</xliff:g> របស់អ្នក។"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ការហៅមិនអាចធ្វើបានទេ ដោយសារមានការហៅមួយនៅក្នុងកម្មវិធីផ្សេង។"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index cbaa203..886ccdf 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -56,7 +56,7 @@
<string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
<string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
<string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
- <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಅಪ್ಲಿಕೇಶನ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
+ <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಆ್ಯಪ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
<string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"<xliff:g id="NEW_APP">%s</xliff:g> ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೇ?"</string>
<string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ಇನ್ನು ಮುಂದೆ ಕರೆಗಳನ್ನು ಸ್ಕ್ರೀನ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ."</string>
<string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಸಂಪರ್ಕಗಳಲ್ಲಿ ಇಲ್ಲದ ಕರೆದಾರರ ಬಗ್ಗೆ ಮಾಹಿತಿಯನ್ನು ನೋಡಲು ಮತ್ತು ಈ ಕರೆಗಳನ್ನು ಬ್ಲಾಕ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ಕರೆಗೆ ಉತ್ತರಿಸುವುದರಿಂದ ನಿಮ್ಮ ಚಾಲ್ತಿಯಲ್ಲಿರುವ ವೀಡಿಯೊ ಕರೆಯು ಅಂತ್ಯಗೊಳ್ಳುತ್ತದೆ"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ಉತ್ತರ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ನಿರಾಕರಿಸಿ"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ಈ ಪ್ರಕಾರದ ಕರೆಗಳನ್ನು ಬೆಂಬಲಿಸುವ ಯಾವುದೇ ಕರೆಮಾಡುವಿಕೆ ಖಾತೆಗಳು ಇಲ್ಲದಿರುವ ಕಾರಣ ಕರೆಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ನಿಮ್ಮ ಸಾಧನದ ಕನೆಕ್ಷನ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ನಿಮ್ಮ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ಕರೆ ಇರುವ ಕಾರಣ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ನಿಮ್ಮ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ಕರೆಗಳ ಕಾರಣ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ಬೇರೊಂದು ಅಪ್ಲಿಕೇಶನ್ನಲ್ಲಿ ಕರೆಯಲ್ಲಿರುವುದರಿಂದ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ."</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index dc793e3..f0b95fd 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"전화를 받으면 진행 중인 화상 통화가 종료됩니다."</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"통화"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"거부"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"이 유형의 전화를 지원하는 전화 계정이 없으므로 전화를 걸 수 없습니다."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"전화를 걸 수 없습니다. 기기의 연결 상태를 확인하세요."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> 통화 중이므로 전화를 걸 수 없습니다."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> 통화 중이므로 전화를 걸 수 없습니다."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"다른 앱에서 통화 중이므로 전화를 걸 수 없습니다."</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 43def8b..ad19dd7 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Чалууга жооп берсеңиз, учурдагы видео чалууңуз бүтүп калат"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Жооп берүү"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Четке кагуу"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Бул түрдөгү чалуударды колдоого алган чалуу аккаунттары жок болгондуктан, чалуу аткарылбай койду."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Чалуу мүмкүн эмес. Түзмөгүңүздүн туташуусун текшериңиз."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Учурда <xliff:g id="OTHER_CALL">%1$s</xliff:g> чалууңуздан улам, башка жерге чала албайсыз."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Учурда <xliff:g id="OTHER_CALL">%1$s</xliff:g> чалууларыңуздан улам, башка жерге чала албайсыз."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Башка колдонмодо чалып жатасыз, ошондуктан чала албайсыз."</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index ff79144..8e43935 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ການຮັບສາຍຈະເປັນການວາງສາຍວິດີໂອທີ່ທ່ານກຳລັງໂທອອກ"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ຮັບສາຍ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ປະຕິເສດ"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ບໍ່ສາມາດໂທໄດ້ເນື່ອງຈາກບໍ່ມີບັນຊີການໂທທີ່ຮອງຮັບການໂທປະເພດນີ້."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ບໍ່ສາມາດໂທອອກໄດ້. ກວດເບິ່ງການເຊື່ອມຕໍ່ຂອງອຸປະກອນຂອງທ່ານ."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກການໂທ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ຂອງທ່ານ"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກການໂທ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ຂອງທ່ານ"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກສາຍໃນແອັບອື່ນ."</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 9454431..04f4c96 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Atsakius bus užbaigtas vykstantis vaizdo skambutis"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Atsakyti"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Atmesti"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Negalima skambinti, nes nėra jokių skambinimo paskyrų, kuriose palaikomi šio tipo skambučiai."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nepavyko paskambinti. Patikrinkite įrenginio ryšį."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Negalima skambinti dėl „<xliff:g id="OTHER_CALL">%1$s</xliff:g>“ skambučio."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Negalima skambinti dėl „<xliff:g id="OTHER_CALL">%1$s</xliff:g>“ skambučių."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Negalima skambinti dėl skambučio kitoje programoje."</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 5ebdd8e..ee807da 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Atbildot uz zvanu, tiks beigts pašreizējais videozvans"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Atbildēt"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Noraidīt"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Nevar veikt zvanu, jo ierīcē nav neviena zvanu konta, kurā tiktu atbalstīti šī veida zvani."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nevar veikt zvanu. Pārbaudiet ierīces savienojumu."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Nevar veikt zvanu notiekoša <xliff:g id="OTHER_CALL">%1$s</xliff:g> zvana dēļ."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Nevar veikt zvanu notiekošu <xliff:g id="OTHER_CALL">%1$s</xliff:g> zvanu dēļ."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Nevar veikt zvanu citā lietotnē notiekoša zvana dēļ."</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 57a3fce..0f6e41f 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ако одговорите, ќе се прекине вашиот тековен видеоповик"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Одговорете"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Одбијте"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Повикот не може да се воспостави затоа што нема сметки за повикување што поддржуваат ваков тип повици."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не може да се оствари повик. Проверете ја врската на уредот."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Не може да се воспостави повик поради вашиот повик на <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Не може да се воспостави повик поради вашите повици на <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Не може да се воспостави повик поради вашиот повик на друга апликација."</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index a6d1626..1301b44 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"കോൾ സ്വീകരിക്കുന്നത് നിങ്ങളുടെ നിലവിലുള്ള വീഡിയോ കോൾ അവസാനിക്കാനിടയാക്കും"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"മറുപടി നൽകുക"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"നിരസിക്കുക"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ഇത്തരം കോളുകൾക്ക് അനുയോജ്യമായ അക്കൗണ്ടുകളൊന്നും ഇല്ലാത്തതിനാൽ കോൾ ചെയ്യാനായില്ല."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"കോൾ ചെയ്യാനാകില്ല. നിങ്ങളുടെ ഉപകരണത്തിന്റെ കണക്ഷൻ പരിശോധിക്കുക."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"നിങ്ങളുടെ <xliff:g id="OTHER_CALL">%1$s</xliff:g> കോൾ കാരണം കോൾ ചെയ്യാനായില്ല."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"നിങ്ങളുടെ <xliff:g id="OTHER_CALL">%1$s</xliff:g> കോളുകൾ കാരണം കോൾ ചെയ്യാനായില്ല."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"മറ്റൊരു ആപ്പിലുള്ള കോൾ കാരണം കോൾ ചെയ്യാനായില്ല."</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 70dde8a..0b26e7e 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Хариулбал таны одоогийн видео дуудлагыг таслах болно"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Хариулах"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Татгалзах"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Энэ төрлийн дуудлага дэмждэг дуудлагын бүртгэл байхгүй тул дуудлага хийх боломжгүй байна."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Дуудлага хийх боломжгүй. Төхөөрөмжийнхөө холболтыг шалгана уу."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Таны <xliff:g id="OTHER_CALL">%1$s</xliff:g> дуудлагаас шалтгаалан дуудлага хийх боломжгүй байна."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Таны <xliff:g id="OTHER_CALL">%1$s</xliff:g> дуудлагаас шалтгаалан дуудлага хийх боломжгүй байна."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Өөр апп доторх дуудлагаас шалтгаалан дуудлага хийх боломжгүй байна."</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index c4438ae..eca7b4d 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"उत्तर देण्यामुळे तुमचा सुरू असलेला व्हिडिओ कॉल समाप्त होईल"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"उत्तर द्या"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"नकार द्या"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"कॉल करू शकत नाही कारण अशाप्रकारच्या कॉलला सपोर्ट करतील अशी कोणतीही कॉलिंग खाती नाहीत."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"कॉल करू शकत नाही. तुमच्या डिव्हाइसचे कनेक्शन तपासणे."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"आपल्या <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉलमुळे कॉल केला जाऊ शकत नाही."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"आपल्या <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉलमुळे कॉल केला जाऊ शकत नाही."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"दुसर्या ॲपमधील कॉलमुळे कॉल केला जाऊ शकत नाही."</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 355502c..ebfffd0 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Menjawab akan menamatkan panggilan video semasa anda"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Jawab"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Tolak"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Panggilan tidak dapat dibuat kerana tiada akaun panggilan yang menyokong panggilan jenis ini."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Tidak dapat membuat panggilan. Semak sambungan peranti anda."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Panggilan tidak dapat dibuat disebabkan panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> anda."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Panggilan tidak dapat dibuat disebabkan panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> anda."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Panggilan tidak dapat dibuat disebabkan panggilan dalam apl lain."</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index c9e5593..e7f0fd4 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ဖုန်းကိုင်လိုက်လျှင် လက်ရှိဗီဒီယိုပြောနေခြင်းကိုဖြတ်ပစ်ပါမည်"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ဖုန်းကိုင်ရန်"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ဖုန်းမကိုင်ရန်"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ဤဖုန်းခေါ်ဆိုမှု အမျိုးအစားကို ပံ့ပိုးပေးသည့် ခေါ်ဆိုမှုအကောင့်များ မရှိသဖြင့် ဖုန်းခေါ်၍ မရပါ။"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ဖုန်းမခေါ်ဆိုနိုင်ပါ။ သင့်စက်၏ ချိတ်ဆက်မှုကို စစ်ပါ။"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> သုံးပြီးပြောနေသည့်အတွက် အထွက်ခေါ်ဆိုမှုကို မပြုလုပ်နိုင်ပါ။"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> သုံးပြီးပြောနေသည့်အတွက် အထွက်ခေါ်ဆိုမှုများကို မပြုလုပ်နိုင်ပါ။"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"အခြားအက်ပ်သုံးပြီးပြောနေသည့်အတွက် အထွက်ခေါ်ဆိုမှုကို မပြုလုပ်နိုင်ပါ။"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 8bebbff..66e6ffc 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Hvis du svarer, avsluttes videosamtalen du er i nå"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Svar"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Avvis"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Anropet kan ikke utføres fordi du ikke har noen ringekontoer som støtter denne typen anrop."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kan ikke ringe. Sjekk tilkoblingen på enheten."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Kan ikke ringe ut på grunn av <xliff:g id="OTHER_CALL">%1$s</xliff:g>-samtalen din."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Kan ikke ringe ut på grunn av <xliff:g id="OTHER_CALL">%1$s</xliff:g>-samtalene dine."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Kan ikke ringe ut på grunn av en samtale i en annen app."</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 44645dc..4aeceef 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>
@@ -72,7 +72,7 @@
<string name="block_button" msgid="485080149164258770">"रोक्नुहोस्"</string>
<string name="non_primary_user" msgid="315564589279622098">"यन्त्रको मालिकले रोकिएका नम्बरहरूलाई हेर्न र व्यवस्थापन गर्न सक्छ।"</string>
<string name="delete_icon_description" msgid="5335959254954774373">"अनब्लक गर्नुहोस्"</string>
- <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"रोक लगाउने काम अस्थायी रूपमा निष्क्रिय छ"</string>
+ <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"रोक लगाउने काम अस्थायी रूपमा अफ छ"</string>
<string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"तपाईँले आपत्कालीन नम्बरमा डायल गरेपछि वा टेक्स्ट म्यासेज पठाएपछि आपत्कालीन सेवाहरूले तपाईँलाई सम्पर्क गर्न सकून् भन्ने कुरा सुनिश्चित गर्न कलमाथिको अवरोध निष्क्रिय गरिन्छ।"</string>
<string name="blocked_numbers_butter_bar_button" msgid="2704456308072489793">"अब पुन:-अन गर्नुहोस्"</string>
<string name="blocked_numbers_number_blocked_message" msgid="4314736791180919167">"<xliff:g id="BLOCKED_NUMBER">%1$s</xliff:g> माथि रोक लगाइयो"</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"जवाफ फर्काउनुले तपाईंको जारी भिडियो कल समाप्त हुनेछ"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"जवाफ दिनुहोस्"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"अस्वीकार गर्नुहोस्"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"यस प्रकारका कलहरूलाई समर्थन गर्ने कुनै पनि कल गर्ने खाता नभएकाले कल गर्न सकिँदैन।"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"कल गर्न सकिएन। आफ्नो डिभाइसको इन्टरनेट जाँच्नुहोस्।"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"तपाईंको <xliff:g id="OTHER_CALL">%1$s</xliff:g> कलका कारण कल गर्न सकिँदैन।"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"तपाईंका <xliff:g id="OTHER_CALL">%1$s</xliff:g> कलहरूका कारण कल गर्न सकिँदैन।"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"अर्को एपमा जारी कलका कारण कल गर्न सकिँदैन।"</string>
diff --git a/res/values-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-nl/strings.xml b/res/values-nl/strings.xml
index 8dfee81..e395ef1 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"Telefoongesprekken"</string>
+ <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"Telefoongesprekken"</string>
<string name="userCallActivityLabel" product="default" msgid="3605391260292846248">"Telefoon"</string>
<string name="unknown" msgid="6993977514360123431">"Onbekend"</string>
<string name="notification_missedCallTitle" msgid="5060387047205532974">"Gemist gesprek"</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Als je opneemt, wordt je actieve videogesprek beëindigd"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Beantwoorden"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Weigeren"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Gesprek kan niet worden geplaatst omdat er geen gespreksaccounts zijn die gesprekken van dit type ondersteunen."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kan niet bellen. Check de verbinding van je apparaat."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Gesprek kan niet worden gestart vanwege je <xliff:g id="OTHER_CALL">%1$s</xliff:g>-gesprek."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Gesprek kan niet worden gestart vanwege je <xliff:g id="OTHER_CALL">%1$s</xliff:g>-gesprekken."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Gesprek kan niet worden gestart vanwege een gesprek in een andere app."</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 6f3ebe3..535583a 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ଉତ୍ତର ଦେବାଦ୍ଵାରା ଆପଣଙ୍କର ଜାରି ରହିଥିବା ଭିଡିଓ କଲ୍ ସମାପ୍ତ ହୋଇଯିବ"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ଉତ୍ତର ଦିଅନ୍ତୁ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ଅସ୍ୱୀକାର"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ଏହି ପ୍ରକାରର କଲ୍ ସମର୍ଥନ କରୁଥିବା କଲିଂ ଆକାଉଣ୍ଟ ନଥିବା ଯୋଗୁଁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"କଲ କରାଯାଇପାରିବ ନାହିଁ। ଆପଣଙ୍କ ଡିଭାଇସର କନେକ୍ସନ ଯାଞ୍ଚ କରନ୍ତୁ।"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ଆପଣଙ୍କର <xliff:g id="OTHER_CALL">%1$s</xliff:g> କଲ୍ ହେତୁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ଆପଣଙ୍କର <xliff:g id="OTHER_CALL">%1$s</xliff:g> କଲ୍ ହେତୁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ଅନ୍ୟ ଆପ୍ରେ କରାଯାଇଥିବା କଲ୍ ହେତୁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index b96a1db..96ee0e8 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ਜਵਾਬ ਦੇਣ ਨਾਲ ਤੁਹਾਡੀ ਜਾਰੀ ਵੀਡੀਓ ਕਾਲ ਸਮਾਪਤ ਹੋ ਜਾਵੇਗੀ"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"ਕਾਲ ਚੁੱਕੋ"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ਕਾਲ ਕੱਟੋ"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ ਕਿਉਂਕਿ ਇੱਥੇ ਅਜਿਹੇ ਕੋਈ ਕਾਲਿੰਗ ਖਾਤੇ ਨਹੀਂ ਹਨ ਜਿਨ੍ਹਾਂ ਵਿੱਚ ਇਸ ਕਿਸਮ ਦੀਆਂ ਕਾਲਾਂ ਦੀ ਸੁਵਿਧਾ ਹੋਵੇ।"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। ਆਪਣੇ ਡੀਵਾਈਸ ਦੇ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ਤੁਹਾਡੀ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ਕਾਲ ਦੇ ਕਾਰਨ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ਤੁਹਾਡੀਆਂ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ਕਾਲਾਂ ਦੇ ਕਾਰਨ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ਕਿਸੇ ਹੋਰ ਐਪ ਵਿੱਚ ਇੱਕ ਕਾਲ ਹੋਣ ਦੇ ਕਾਰਨ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।"</string>
@@ -109,7 +109,7 @@
<string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"ਕਾਲ ਬਲਾਕ ਕਰਨਾ"</string>
<string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"ਨੰਬਰ ਜੋ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਵਿੱਚ ਨਹੀਂ ਹਨ"</string>
<string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"ਉਹ ਨੰਬਰ ਬਲਾਕ ਕਰੋ ਜੋ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਵਿੱਚ ਨਹੀਂ ਹਨ"</string>
- <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"ਨਿੱਜੀ"</string>
+ <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"ਪ੍ਰਾਈਵੇਟ"</string>
<string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"ਉਹ ਕਾਲਰ ਬਲਾਕ ਕਰੋ ਜਿਨ੍ਹਾਂ ਦਾ ਨੰਬਰ ਨਹੀਂ ਦਿਖਾਈ ਦਿੰਦਾ ਹੈ"</string>
<string name="phone_settings_payphone_txt" msgid="5003987966052543965">"ਜਨਤਕ ਫ਼ੋਨ"</string>
<string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"ਜਨਤਕ ਫ਼ੋਨਾਂ ਵਾਲੀਆਂ ਕਾਲਾਂ ਬਲਾਕ ਕਰੋ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index df5d29e..23776f5 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Jeśli odbierzesz połączenie, zakończysz rozmowę wideo"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Odbierz"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Odrzuć"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Nie można nawiązać połączenia, ponieważ nie ma żadnego konta, które obsługuje połączenia tego typu."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nie udało się zadzwonić. Sprawdź połączenie urządzenia."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Nie możesz zadzwonić z powodu trwającej rozmowy w <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Nie możesz zadzwonić z powodu trwających rozmów w <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Nie możesz zadzwonić z powodu trwającej rozmowy w innej aplikacji."</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 5fbe1d3..122615a 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ao atender, a sua videochamada em curso será terminada"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Atender"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Recusar"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Não é possível efetuar a chamada porque não existem contas de chamadas que suportem chamadas deste tipo."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Não é possível fazer a chamada. Verifique a ligação do seu dispositivo."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Não é possível efetuar a chamada devido à sua chamada do <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Não é possível efetuar a chamada devido às suas chamadas do <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Não é possível efetuar a chamada devido a uma chamada noutra app."</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index a7fc3c7..e302ea6 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -50,8 +50,8 @@
<string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Este aplicativo não pode fazer chamadas sem a permissão do smartphone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Para realizar uma chamada, digite um número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="5754746140185781159">"No momento, não é possível adicionar a chamada."</string>
- <string name="no_vm_number" msgid="2179959110602180844">"Número correio de voz ausente"</string>
- <string name="no_vm_number_msg" msgid="1339245731058529388">"Não há um número correio de voz armazenado no chip."</string>
+ <string name="no_vm_number" msgid="2179959110602180844">"Número do correio de voz ausente"</string>
+ <string name="no_vm_number_msg" msgid="1339245731058529388">"Não há um número do correio de voz armazenado no chip."</string>
<string name="add_vm_number_str" msgid="5179510133063168998">"Adicionar número"</string>
<string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Usar o <xliff:g id="NEW_APP">%s</xliff:g> como seu app de telefone padrão?"</string>
<string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Definir padrão"</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Se você atender, a videochamada em andamento será encerrada"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Atender"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Recusar"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Não é possível ligar porque não há contas compatíveis com chamadas deste tipo."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Não foi possível fazer a chamada. Verifique a conexão do dispositivo."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Não é possível ligar com uma chamada em andamento no <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Não é possível ligar com chamadas em andamento no <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Não é possível ligar com uma chamada em andamento em outro aplicativo."</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 8e485d0..fe5ad93 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Dacă răspunzi, apelul video în curs va fi încheiat."</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Răspunde"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Respinge"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Apelul nu poate fi inițiat deoarece nu există conturi pentru apelare compatibile cu apeluri de acest tip."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Apelul nu poate fi inițiat. Verifică conexiunea dispozitivului."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Apelul nu poate fi inițiat din cauza apelului <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Apelul nu poate fi inițiat din cauza apelurilor <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Apelul nu poate fi inițiat din cauza unui apel din altă aplicație."</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 67ab2e9..cc69d40 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Если вы ответите, текущий видеовызов будет завершен."</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Ответить"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Отклонить"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Невозможно позвонить, так как нет аккаунтов, которые поддерживают вызовы этого типа."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не удалось позвонить. Проверьте подключение устройства."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Вы не можете отправить вызов, пока не завершите другой в приложении <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Вы не можете отправить вызов, пока не завершите другие в приложении <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Вы не можете отправить новый вызов, пока не завершите текущий в другом приложении"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 71442e0..2ea058f 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"පිළිතුරු දීම ඔබේ යන වීඩියෝ ඇමතුම අවසන් කරනු ඇත"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"පිළිතුරු දෙන්න"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ප්රතික්ෂේප කරන්න"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"මෙම වර්ගයේ ඇමතුම්වලට සහාය දක්වන ඇමතීමේ ගිණුම් නොමැති නිසා ඇමතුම ගැනීමට නොහැකිය."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ඇමතුම ගත නොහැක. ඔබේ උපාංගයේ සම්බන්ධතාවය පරීක්ෂා කරන්න."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ඔබේ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ඇමතුම හේතුවෙන් ඇමතුම ගැනීමට නොහැකිය."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ඔබේ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ඇමතුම් හේතුවෙන් ඇමතුම ගැනීමට නොහැකිය."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"වෙනත් යෙදුමක ඇමතුමක් හේතුවෙන් ඇමතුම ගැනීමට නොහැකිය."</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index a001130..fc7108a 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Prijatím hovoru ukončíte prebiehajúci videohovor"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Prijať"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Odmietnuť"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Hovor sa nedá uskutočniť, pretože nie je k dispozícii žiaden účet, ktorý by tento typ hovorov podporoval."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nedá sa volať. Skontrolujte pripojenie zariadenia."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Hovor sa nedá uskutočniť, pretože prebieha hovor <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Hovor sa nedá uskutočniť, pretože prebiehajú hovory <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Hovor sa nedá uskutočniť, pretože prebieha hovor v inej aplikácii."</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 994bc7e..7ee0b0b 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Če sprejmete, bo končan aktivni videoklic"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Sprejmi"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Zavrni"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Klica ni mogoče vzpostaviti, ker ni računov za klicanje, ki podpirajo tovrstne klice."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Klica ni mogoče vzpostaviti. Preverite povezavo naprave."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Klica ni mogoče vzpostaviti zaradi klica prek aplikacije <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Klica ni mogoče vzpostaviti zaradi klicev prek aplikacije <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Klica ni mogoče vzpostaviti zaradi klica prek druge aplikacije."</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 89ae852..7d8045a 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Përgjigjja do ta mbyllë telefonatën me video në vazhdim"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Përgjigju"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Refuzo"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Telefonata nuk mund të kryhet pasi nuk ka asnjë llogari telefonatash që i mbështet telefonatat e këtij lloji."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Telefonata nuk mund të kryhet. Kontrollo lidhjen e pajisjes sate."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Telefonata nuk mund të kryhet për shkak të telefonatës tënde të <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Telefonata nuk mund të kryhet për shkak të telefonatave të tua të <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Telefonata nuk mund të kryhet për shkak të një telefonate në një aplikacion tjetër."</string>
@@ -126,7 +126,7 @@
<string name="cancel" msgid="6733466216239934756">"Anulo"</string>
<string name="back" msgid="6915955601805550206">"Pas"</string>
<string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Receptori"</string>
- <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth-i"</string>
<string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Kufje me tel"</string>
<string name="callendpoint_name_speaker" msgid="1971760468695323189">"Altoparlant"</string>
<string name="callendpoint_name_streaming" msgid="2337595450408275576">"E jashtme"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 1134380..148cb14 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ако одговорите, завршићете видео позив који је у току"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Одговори"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Одбиј"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Упућивање позива није могуће јер немате ниједан налог за позивање који подржава позиве овог типа."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Позивање није успело. Проверите везу уређаја."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Не можете да упутите позив због <xliff:g id="OTHER_CALL">%1$s</xliff:g> позива."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Не можете да упутите позив због <xliff:g id="OTHER_CALL">%1$s</xliff:g> позива."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Не можете да упутите позив због позива у другој апликацији."</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index c6f6ec9..d4a930c 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Det pågående videosamtalet avslutas om du svarar"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Svara"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Avvisa"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Det går inte att ringa på grund av att det inte finns uppringningskonton som stöder den här samtalstypen."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Det går inte att ringa samtalet. Kontrollera enhetens anslutning."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Det går inte att ringa på grund av samtalet via <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Det går inte att ringa på grund av samtalen via <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Det går inte att ringa på grund av ett samtal via en annan app."</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index ef58c00..ac0518d 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ukijibu utakata simu yako ya video inayoendelea"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Jibu"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Kataa"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Haiwezi kupiga simu kwa sababu hakuna akaunti za kupiga simu zinazoweza kupiga aina hii ya simu."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Imeshindwa kupiga simu. Kagua muunganisho wa kifaa chako."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Haiwezekani kupiga kwa sababu ya simu yako ya <xliff:g id="OTHER_CALL">%1$s</xliff:g> inayoendelea."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Haiwezekani kupiga kwa sababu ya simu zako za <xliff:g id="OTHER_CALL">%1$s</xliff:g> zinazoendelea."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Haiwezekani kwa sababu kuna simu inayoendelea kwenye programu nyingine."</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 9f37d87..57c70f4 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"பதிலளித்தால், செயலில் உள்ள வீடியோ அழைப்பு துண்டிக்கப்படும்"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"பதிலளி"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"நிராகரி"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"இந்த வகை அழைப்புகளை ஆதரிக்கும் அழைப்புக் கணக்குகள் இல்லாததால், அழைப்பை மேற்கொள்ள முடியாது."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"அழைப்பை மேற்கொள்ள முடியவில்லை. உங்கள் சாதனத்தின் இணைப்பைச் சரிபார்க்கவும்."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> அழைப்பு செயலில் உள்ளதால், புதிய அழைப்பைச் செய்ய முடியாது."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> அழைப்புகள் செயலில் உள்ளதால், புதிய அழைப்பைச் செய்ய முடியாது."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"மற்றொரு பயன்பாட்டில் அழைப்பு செயலில் உள்ளதால், புதிய அழைப்பைச் செய்ய முடியாது."</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 8f8a23e..22f4b8a 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"సమాధానమివ్వడం వలన మీ కొనసాగుతున్న వీడియో కాల్ ముగుస్తుంది"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"సమాధానమివ్వండి"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"తిరస్కరించు"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"కాల్ చేయడం సాధ్యపడదు ఎందుకంటే, ఈ రకమైన కాల్స్కు మద్దతిచ్చే కాల్ చేయడానికి ఉపయోగించే ఖాతాలు లేవు."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"కాల్ చేయడం సాధ్యపడదు. మీ పరికర కనెక్షన్ను చెక్ చేయండి."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"మీ <xliff:g id="OTHER_CALL">%1$s</xliff:g> కాల్ కొనసాగుతున్నందున కాల్ చేయడం సాధ్యపడదు."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"మీ <xliff:g id="OTHER_CALL">%1$s</xliff:g> కాల్స్ కొనసాగుతున్నందున కాల్ చేయడం సాధ్యపడదు."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"వేరొక యాప్లో కాల్ కొనసాగుతున్నందున కాల్ చేయడం సాధ్యపడదు."</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index b8dc9f0..e3a20b1 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"การรับสายนี้จะวางสาย Hangouts วิดีโอที่สนทนาอยู่"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"รับสาย"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"ปฏิเสธ"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"การโทรไม่สำเร็จเนื่องจากไม่มีบัญชีการโทรที่รองรับการโทรประเภทนี้"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"โทรออกไม่ได้ โปรดตรวจสอบการเชื่อมต่อของอุปกรณ์"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ไม่สามารถโทรออกได้เนื่องจากกำลังใช้สายอยู่ใน <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ไม่สามารถโทรออกได้เนื่องจากกำลังใช้สายอยู่ใน <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ไม่สามารถโทรออกได้เนื่องจากกำลังใช้สายอยู่ในแอปอื่น"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 91e1b33..001a19a 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Kung sasagutin, matatapos ang iyong kasalukuyang video call"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Sagutin"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Tanggihan"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Hindi maisasagawa ang tawag dahil walang account sa pagtawag na sumusuporta sa ganitong uri ng mga tawag."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Hindi makatawag. Suriin ang koneksyon ng iyong device."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Hindi makakatawag dahil sa iyong tawag sa <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Hindi makakatawag dahil sa iyong mga tawag sa <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Hindi makakatawag dahil sa isang tawag sa isa pang app."</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 0aa2e20..1924d92 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Cevapladığınızda, devam eden görüntülü görüşme sona erecek"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Cevapla"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Reddet"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Bu tür görüşmeleri destekleyen bir arama hesabı olmadığı için arama yapılamıyor."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Arama yapılamıyor. Cihazınızın bağlantısını kontrol edin."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Devam eden <xliff:g id="OTHER_CALL">%1$s</xliff:g> çağrınız nedeniyle telefon araması yapılamıyor."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Devam eden <xliff:g id="OTHER_CALL">%1$s</xliff:g> çağrılarınız nedeniyle telefon araması yapılamıyor."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Başka bir uygulamada devam eden çağrınız nedeniyle telefon araması yapılamıyor."</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index a4d01d1..2d4f5bc 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Якщо відповісти на виклик, поточний відеодзвінок завершиться"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Відповісти"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Відхилити"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Такі виклики не підтримуються. Немає потрібного облікового запису чи сервісу."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не вдається здійснити виклик. Перевірте підключення пристрою."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Неможливо зателефонувати через поточний виклик у <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Неможливо зателефонувати через поточні виклики в <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Неможливо зателефонувати через поточний виклик в іншому додатку."</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 6649f42..b09f244 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"جواب دینا آپ کی جاری ویڈیو کال کو ختم کر دے گا"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"جواب دیں"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"مسترد کریں"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"کال نہیں کی جا سکی کیونکہ اس قسم کی کالز کو سپورٹ کرنے والا کوئی کالنگ اکاؤنٹ نہیں ہے۔"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"کال نہیں کر سکتے۔ اپنے آلے کا کنکشن چیک کریں۔"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"آپ کی <xliff:g id="OTHER_CALL">%1$s</xliff:g> کال کی وجہ سے کال نہیں کی جاسکتی۔"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"آپ کی <xliff:g id="OTHER_CALL">%1$s</xliff:g> کالز کی وجہ سے کالز نہیں کی جاسکتیں۔"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"کسی دوسری ایپ میں موجود کال کی کی وجہ سے کال نہیں کی جا سکتی۔"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index c6805ea..ff04903 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Chaqiruvga javob berilsa, joriy video suhbat tugatiladi."</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Javob berish"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Rad etish"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Telefon qilish imkonsiz, chunki bunday turdagi chaqiruvni qo‘llab-quvvatlaydigan hisob yo‘q."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Telefon ishlamaydi. Qurilma aloqasini tekshiring."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Joriy <xliff:g id="OTHER_CALL">%1$s</xliff:g> qo‘ng‘ir. tufayli boshqa raqamni chaqirib bo‘lmaydi."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Joriy <xliff:g id="OTHER_CALL">%1$s</xliff:g> qo‘ng‘ir-r tufayli boshqa raqamni chaqirib bo‘lmaydi."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Boshqa ilovadagi joriy qo‘ng‘iroq tufayli boshqa raqamni chaqirib bo‘lmaydi."</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ae2e79..142026c 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Trả lời sẽ kết thúc cuộc gọi video đang diễn ra của bạn"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Trả lời"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Từ chối"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Không thể thực hiện cuộc gọi do không có tài khoản hỗ trợ loại cuộc gọi này."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Không thể gọi điện. Hãy kiểm tra kết nối của thiết bị."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Không thể thực hiện cuộc gọi do cuộc gọi <xliff:g id="OTHER_CALL">%1$s</xliff:g> của bạn."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Không thể thực hiện cuộc gọi do cuộc gọi <xliff:g id="OTHER_CALL">%1$s</xliff:g> của bạn."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Không thể thực hiện cuộc gọi do có cuộc gọi trong một ứng dụng khác."</string>
@@ -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-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 1ef0a55..7cb8a7a 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"如果接听此来电,您当前的视频通话会中断。"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"接听"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"拒接"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"无法拨出电话,因为没有通话账号支持拨打这类电话。"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"无法拨打电话。请检查设备的连接情况。"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"由于当前正在通过其他应用通话,因此无法拨打电话。"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 0140f26..213255a 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"如果接聽,你進行中的視像通話將會結束"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"接聽"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"拒絕"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"沒有通話帳戶支援這類通話,因此無法撥打電話。"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"無法撥打電話。請檢查裝置是否正確連接。"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"由於你已在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,因此無法撥打電話。"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"由於你已在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,因此無法撥打電話。"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"由於已在另一個應用程式中進行通話,因此無法撥打電話。"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index eeb98b5..287f627 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"接聽之後,你正在進行的視訊通話就會結束"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"接聽"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"拒接"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"你尚未設定支援這類通話的通話帳戶,因此無法撥打電話。"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"無法撥打電話,請檢查裝置的藍牙連線。"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"你正在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,因此無法撥打電話。"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"你正在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,所以無法撥打電話。"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"你正在使用其他應用程式進行通話,因此無法撥打電話。"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index faee0d9..8d0437d 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ukuphendula kuzoqeda ikholi yakho yevidiyo eqhubekayo"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"Phendula"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"Yenqaba"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ikholi ayikwazi ukubekwa ngoba awasekho ama-akhawunti okushaya asekela amakholi walolu hlobo."</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Ayikwazi ukwenza ikholi. Hlola ukuxhumeka kwedivayisi yakho."</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ikholi ayikwazi ukwenziwa ngenxa yekholi yakho ye-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ikholi ayikwazi ukwenziwa ngenxa yamakholi akho e-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ikholi ayikwazi ukwenziwa ngenxa yekholi kolunye uhlelo lokusebenza."</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 15f765b..8ebbd86 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -49,8 +49,10 @@
<bool name="grant_location_permission_enabled">false</bool>
<!-- When true, a simple full intensity on/off vibration pattern will be used when calls ring.
- When false, a fancy vibration pattern which ramps up and down will be used.
- Devices should overlay this value based on the type of vibration hardware they employ. -->
+
+ When false, the vibration effect serialized in the raw `default_ringtone_vibration_effect`
+ resource (under `frameworks/base/core/res/res/raw/`) is used. Devices should overlay this
+ value based on the type of vibration hardware they employ. -->
<bool name="use_simple_vibration_pattern">false</bool>
<!-- Threshold for the X+Y component of gravity needed for the device orientation to be
@@ -77,4 +79,17 @@
<!-- When true, the options in the call blocking settings to block unavailable and unknown
callers are combined into a single toggle. -->
<bool name="combine_options_to_block_unavailable_and_unknown_callers">true</bool>
+
+ <!-- When true, skip fetching quick reply response -->
+ <bool name="skip_loading_canned_text_response">false</bool>
+
+ <!-- When set, telecom will skip fetching incoming caller info for this account -->
+ <string name="skip_incoming_caller_info_account_package"></string>
+
+ <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/strings.xml b/res/values/strings.xml
index ec278f0..aefd2e6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -290,9 +290,11 @@
<!-- Error message shown to the user when an outgoing call cannot be placed because there no
calling service is present on the device which supports this call type.
- This is typically encountered when the user tries to dial a SIP/VOIP call, but there are
- no calling services present which support SIP calling. [CHAR LIMIT=none] -->
- <string name="cant_call_due_to_no_supported_service">Call cannot be placed because there are no calling accounts which support calls of this type.</string>
+ This can happen on a device such as a watch or tablet which provides calling using a
+ service that may not be available all the time. For example, a watch may rely on Bluetooth
+ to be enabled for calling to work; when Bluetooth is disabled calling would not work.
+ [CHAR LIMIT=none] -->
+ <string name="cant_call_due_to_no_supported_service">Can\'t make call. Check your device\'s connection.</string>
<!-- Error message shown to the user when an outgoing call cannot be placed due to an ongoing
phone call in a third-party app. For example:
diff --git a/res/values/styles.xml b/res/values/styles.xml
index c8b24d3..0660fd5 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -26,12 +26,29 @@
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
</style>
+ <style name="Theme.Telecomm.UserCallActivityNoSplash" parent="@android:style/Theme.DeviceDefault.Light">
+ <item name="android:forceDarkAllowed">true</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+ <item name="android:windowDisablePreview">true</item>
+ </style>
+
<style name="Theme.Telecom.DialerSettings" parent="@android:style/Theme.DeviceDefault.Light">
<item name="android:forceDarkAllowed">true</item>
<item name="android:actionBarStyle">@style/TelecomDialerSettingsActionBarStyle</item>
<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">
@@ -61,6 +78,7 @@
</style>
<style name="BlockedNumbersButton" parent="BlockedNumbersTextPrimary2">
+ <item name="android:textColor">#202124</item>
</style>
<style name="BlockedNumbersTextHead1"
diff --git a/res/xml/activity_blocked_numbers.xml b/res/xml/activity_blocked_numbers.xml
index e77184d..b6298e9 100644
--- a/res/xml/activity_blocked_numbers.xml
+++ b/res/xml/activity_blocked_numbers.xml
@@ -41,8 +41,8 @@
android:layout_height="wrap_content"
android:text="@string/non_primary_user"
android:paddingTop="@dimen/blocked_numbers_large_padding"
- android:paddingLeft="@dimen/blocked_numbers_large_padding"
- android:paddingRight="@dimen/blocked_numbers_large_padding"
+ android:paddingStart="@dimen/blocked_numbers_large_padding"
+ android:paddingEnd="@dimen/blocked_numbers_large_padding"
style="@style/BlockedNumbersTextPrimary2"
android:visibility="gone" />
@@ -62,8 +62,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/blocked_numbers_large_padding"
- android:paddingLeft="@dimen/blocked_numbers_large_padding"
- android:paddingRight="@dimen/blocked_numbers_large_padding">
+ android:paddingStart="@dimen/blocked_numbers_large_padding"
+ android:paddingEnd="@dimen/blocked_numbers_large_padding">
<TextView
android:layout_width="wrap_content"
diff --git a/res/xml/blocking_suppressed_butterbar.xml b/res/xml/blocking_suppressed_butterbar.xml
index 8b941b9..2947340 100644
--- a/res/xml/blocking_suppressed_butterbar.xml
+++ b/res/xml/blocking_suppressed_butterbar.xml
@@ -25,19 +25,19 @@
android:id="@+id/icon"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
- android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
android:paddingTop="@dimen/blocked_numbers_large_padding"
- android:paddingRight="@dimen/blocked_numbers_large_padding"
- android:paddingLeft="@dimen/blocked_numbers_large_padding"
+ android:paddingEnd="@dimen/blocked_numbers_large_padding"
+ android:paddingStart="@dimen/blocked_numbers_large_padding"
android:src="@drawable/ic_status_blocked_orange_40dp"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_toRightOf="@id/icon"
+ android:layout_toEndOf="@id/icon"
android:paddingTop="@dimen/blocked_numbers_large_padding"
- android:paddingRight="@dimen/blocked_numbers_large_padding"
+ android:paddingEnd="@dimen/blocked_numbers_large_padding"
android:text="@string/blocked_numbers_butter_bar_title"
style="@style/BlockedNumbersTextPrimary2" />
@@ -45,11 +45,11 @@
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_toRightOf="@id/icon"
+ android:layout_toEndOf="@id/icon"
android:layout_below="@id/title"
android:paddingTop="@dimen/blocked_numbers_large_padding"
android:paddingBottom="@dimen/blocked_numbers_large_padding"
- android:paddingRight="@dimen/blocked_numbers_large_padding"
+ android:paddingEnd="@dimen/blocked_numbers_large_padding"
android:text="@string/blocked_numbers_butter_bar_body"
style="@style/BlockedNumbersTextSecondary" />
@@ -57,9 +57,9 @@
android:id="@+id/reenable_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_toRightOf="@id/icon"
+ android:layout_toEndOf="@id/icon"
android:layout_below="@id/description"
- android:paddingRight="@dimen/blocked_numbers_large_padding"
+ android:paddingEnd="@dimen/blocked_numbers_large_padding"
android:text="@string/blocked_numbers_butter_bar_button"
style="@style/BlockedNumbersButton"
android:background="?android:attr/selectableItemBackgroundBorderless" />
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index bbcf858..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);
- 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/AppLabelProxy.java b/src/com/android/server/telecom/AppLabelProxy.java
index 7c00f28..c4d83dd 100644
--- a/src/com/android/server/telecom/AppLabelProxy.java
+++ b/src/com/android/server/telecom/AppLabelProxy.java
@@ -16,8 +16,11 @@
package com.android.server.telecom;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import com.android.server.telecom.flags.FeatureFlags;
+import android.os.UserHandle;
import android.telecom.Log;
/**
@@ -30,15 +33,34 @@
class Util {
/**
* Default impl of getAppLabel.
- * @param pm PackageManager instance
+ * @param context Context instance that is not necessarily associated with the correct user.
+ * @param userHandle UserHandle instance of the user that is associated with the app.
* @param packageName package name to look up.
*/
- public static CharSequence getAppLabel(PackageManager pm, String packageName) {
+ public static CharSequence getAppLabel(Context context, UserHandle userHandle,
+ String packageName, FeatureFlags featureFlags) {
try {
- ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
- CharSequence result = pm.getApplicationLabel(info);
- Log.i(LOG_TAG, "package %s: name is %s", packageName, result);
- return result;
+ if (featureFlags.telecomAppLabelProxyHsumAware()){
+ Context userContext = context.createContextAsUser(userHandle, 0 /* flags */);
+ PackageManager userPackageManager = userContext.getPackageManager();
+ if (userPackageManager == null) {
+ Log.w(LOG_TAG, "Could not determine app label since PackageManager is "
+ + "null. Package name is %s", packageName);
+ return null;
+ }
+ ApplicationInfo info = userPackageManager.getApplicationInfo(packageName, 0);
+ CharSequence result = userPackageManager.getApplicationLabel(info);
+ Log.i(LOG_TAG, "package %s: name is %s for user = %s", packageName, result,
+ userHandle.toString());
+ return result;
+ } else {
+ // Legacy code path:
+ PackageManager pm = context.getPackageManager();
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+ CharSequence result = pm.getApplicationLabel(info);
+ Log.i(LOG_TAG, "package %s: name is %s", packageName, result);
+ return result;
+ }
} catch (PackageManager.NameNotFoundException nnfe) {
Log.w(LOG_TAG, "Could not determine app label. Package name is %s", packageName);
}
@@ -47,5 +69,5 @@
}
}
- CharSequence getAppLabel(String packageName);
+ CharSequence getAppLabel(String packageName, UserHandle userHandle);
}
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 912305b..3b5e342 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -26,6 +26,8 @@
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
@@ -81,16 +83,17 @@
* If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
* the volume of the ringtone as it plays.
*
- * @param ringtoneSupplier The {@link Ringtone} factory.
+ * @param ringtoneInfoSupplier The {@link Ringtone} factory.
* @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
* @param isHfpDeviceConnected True if there is a HFP BT device connected, false otherwise.
*/
- public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
- BiConsumer<Ringtone, Boolean> ringtoneConsumer, boolean isHfpDeviceConnected) {
+ public void play(@NonNull Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier,
+ BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer,
+ boolean isHfpDeviceConnected) {
Log.d(this, "Posting play.");
mIsPlaying = true;
SomeArgs args = SomeArgs.obtain();
- args.arg1 = ringtoneSupplier;
+ args.arg1 = ringtoneInfoSupplier;
args.arg2 = ringtoneConsumer;
args.arg3 = Log.createSubsession();
args.arg4 = prepareRingingReadyLatch(isHfpDeviceConnected);
@@ -209,8 +212,10 @@
* Starts the actual playback of the ringtone. Executes on ringtone-thread.
*/
private void handlePlay(SomeArgs args) {
- Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
- BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
+ Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier =
+ (Supplier<Pair<Uri, Ringtone>>) args.arg1;
+ BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer =
+ (BiConsumer<Pair<Uri, Ringtone>, Boolean>) args.arg2;
Session session = (Session) args.arg3;
CountDownLatch ringingReadyLatch = (CountDownLatch) args.arg4;
args.recycle();
@@ -226,6 +231,7 @@
return;
}
Ringtone ringtone = null;
+ Uri ringtoneUri = null;
boolean hasStopped = false;
try {
try {
@@ -236,7 +242,11 @@
} catch (InterruptedException e) {
Log.w(this, "handlePlay: latch exception: " + e);
}
- ringtone = ringtoneSupplier.get();
+ if (ringtoneInfoSupplier != null && ringtoneInfoSupplier.get() != null) {
+ ringtoneUri = ringtoneInfoSupplier.get().first;
+ ringtone = ringtoneInfoSupplier.get().second;
+ }
+
// Ringtone supply can be slow or stop command could have been issued while waiting
// for BT to move to CONNECTED state. Re-check for stop event.
if (mHandler.hasMessages(EVENT_STOP)) {
@@ -253,8 +263,7 @@
Log.w(this, "No ringtone was found bail out from playing.");
return;
}
- Uri uri = mRingtone.getUri();
- String uriString = (uri != null ? uri.toSafeString() : "");
+ String uriString = ringtoneUri != null ? ringtoneUri.toSafeString() : "";
Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
mRingtone.setLooping(true);
if (mRingtone.isPlaying()) {
@@ -265,7 +274,7 @@
Log.i(this, "Play ringtone, looping.");
} finally {
removePendingRingingReadyLatch(ringingReadyLatch);
- ringtoneConsumer.accept(ringtone, hasStopped);
+ ringtoneConsumer.accept(new Pair(ringtoneUri, ringtone), hasStopped);
}
} finally {
Log.cancelSubsession(session);
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
new file mode 100644
index 0000000..d3ed77d
--- /dev/null
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+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.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+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;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+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;
+
+public class AudioRoute {
+ public static class Factory {
+ private final ScheduledExecutorService mScheduledExecutorService =
+ new ScheduledThreadPoolExecutor(1);
+ 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();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException("Error when creating requested audio route");
+ }
+ }
+ 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, "createRetry; type=%s, address=%s, retryCount=%d",
+ DEVICE_TYPE_STRINGS.get(type), bluetoothAddress, retryCount);
+ AudioDeviceInfo routeInfo = null;
+ List<AudioDeviceInfo> infos = audioManager.getAvailableCommunicationDevices();
+ List<Integer> possibleInfoTypes = AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.get(type);
+ for (AudioDeviceInfo info : infos) {
+ Log.i(this, "type: " + info.getType());
+ if (possibleInfoTypes != null && possibleInfoTypes.contains(info.getType())) {
+ if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
+ if (bluetoothAddress.equals(info.getAddress())) {
+ routeInfo = info;
+ break;
+ }
+ } else {
+ routeInfo = info;
+ break;
+ }
+ }
+ }
+ // 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));
+ }
+ }
+ }
+
+ private static final long RETRY_TIME_DELAY = 500L;
+ private static final int MAX_CONNECTION_RETRIES = 2;
+ public static final int TYPE_INVALID = 0;
+ public static final int TYPE_EARPIECE = 1;
+ public static final int TYPE_WIRED = 2;
+ public static final int TYPE_SPEAKER = 3;
+ public static final int TYPE_DOCK = 4;
+ public static final int TYPE_BLUETOOTH_SCO = 5;
+ public static final int TYPE_BLUETOOTH_HA = 6;
+ public static final int TYPE_BLUETOOTH_LE = 7;
+ public static final int TYPE_STREAMING = 8;
+ // Used by auto
+ public static final int TYPE_BUS = 9;
+ @IntDef(prefix = "TYPE", value = {
+ TYPE_INVALID,
+ TYPE_EARPIECE,
+ TYPE_WIRED,
+ TYPE_SPEAKER,
+ TYPE_DOCK,
+ TYPE_BLUETOOTH_SCO,
+ TYPE_BLUETOOTH_HA,
+ TYPE_BLUETOOTH_LE,
+ TYPE_STREAMING,
+ TYPE_BUS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AudioRouteType {}
+
+ private @AudioRouteType int mAudioRouteType;
+ private String mBluetoothAddress;
+ private AudioDeviceInfo mInfo;
+ private boolean mIsDestRouteForWatch;
+ public static final Set<Integer> BT_AUDIO_DEVICE_INFO_TYPES = Set.of(
+ AudioDeviceInfo.TYPE_BLE_HEADSET,
+ AudioDeviceInfo.TYPE_BLE_SPEAKER,
+ AudioDeviceInfo.TYPE_BLE_BROADCAST,
+ AudioDeviceInfo.TYPE_HEARING_AID,
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO
+ );
+
+ public static final Set<Integer> BT_AUDIO_ROUTE_TYPES = Set.of(
+ AudioRoute.TYPE_BLUETOOTH_SCO,
+ AudioRoute.TYPE_BLUETOOTH_HA,
+ AudioRoute.TYPE_BLUETOOTH_LE
+ );
+
+ public static final HashMap<Integer, String> DEVICE_TYPE_STRINGS;
+ static {
+ DEVICE_TYPE_STRINGS = new HashMap<>();
+ DEVICE_TYPE_STRINGS.put(TYPE_EARPIECE, "TYPE_EARPIECE");
+ DEVICE_TYPE_STRINGS.put(TYPE_WIRED, "TYPE_WIRED_HEADSET");
+ DEVICE_TYPE_STRINGS.put(TYPE_SPEAKER, "TYPE_SPEAKER");
+ DEVICE_TYPE_STRINGS.put(TYPE_DOCK, "TYPE_DOCK");
+ DEVICE_TYPE_STRINGS.put(TYPE_BUS, "TYPE_BUS");
+ DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO");
+ DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA");
+ DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE");
+ DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING");
+ }
+
+ public static final HashMap<Integer, Integer> DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE;
+ static {
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE = new HashMap<>();
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+ TYPE_EARPIECE);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ TYPE_SPEAKER);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_WIRED);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, TYPE_WIRED);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+ TYPE_BLUETOOTH_SCO);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_DEVICE, TYPE_WIRED);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_ACCESSORY, TYPE_WIRED);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK, TYPE_DOCK);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_HEADSET, TYPE_WIRED);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_HEARING_AID,
+ TYPE_BLUETOOTH_HA);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_HEADSET,
+ TYPE_BLUETOOTH_LE);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_SPEAKER,
+ TYPE_BLUETOOTH_LE);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_BROADCAST,
+ TYPE_BLUETOOTH_LE);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK_ANALOG, TYPE_DOCK);
+ DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUS, TYPE_BUS);
+ }
+
+ private static final HashMap<Integer, List<Integer>> AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE;
+ static {
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE = new HashMap<>();
+ List<Integer> earpieceDeviceInfoTypes = new ArrayList<>();
+ earpieceDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_EARPIECE, earpieceDeviceInfoTypes);
+
+ List<Integer> wiredDeviceInfoTypes = new ArrayList<>();
+ wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES);
+ wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE);
+ wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_ACCESSORY);
+ wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_WIRED, wiredDeviceInfoTypes);
+
+ List<Integer> speakerDeviceInfoTypes = new ArrayList<>();
+ speakerDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_SPEAKER, speakerDeviceInfoTypes);
+
+ List<Integer> dockDeviceInfoTypes = new ArrayList<>();
+ dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK);
+ dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK_ANALOG);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_DOCK, dockDeviceInfoTypes);
+
+ List<Integer> busDeviceInfoTypes = new ArrayList<>();
+ busDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUS);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BUS, busDeviceInfoTypes);
+
+ List<Integer> bluetoothScoDeviceInfoTypes = new ArrayList<>();
+ bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+ bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_SCO, bluetoothScoDeviceInfoTypes);
+
+ List<Integer> bluetoothHearingAidDeviceInfoTypes = new ArrayList<>();
+ bluetoothHearingAidDeviceInfoTypes.add(AudioDeviceInfo.TYPE_HEARING_AID);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_HA,
+ bluetoothHearingAidDeviceInfoTypes);
+
+ List<Integer> bluetoothLeDeviceInfoTypes = new ArrayList<>();
+ bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_HEADSET);
+ bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_SPEAKER);
+ bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_BROADCAST);
+ AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
+ }
+
+ public int getType() {
+ return mAudioRouteType;
+ }
+
+ public boolean isWatch() {
+ return mIsDestRouteForWatch;
+ }
+
+ String getBluetoothAddress() {
+ return mBluetoothAddress;
+ }
+
+ // Invoked when entered pending route whose dest route is this route
+ void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+ BluetoothDevice device, AudioManager audioManager,
+ BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
+ Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%s)", active,
+ DEVICE_TYPE_STRINGS.get(mAudioRouteType));
+ if (pendingAudioRoute.isActive() && !active) {
+ clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+ } else if (active) {
+ // 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) {
+ // Set whether the dest route is for the watch
+ mIsDestRouteForWatch = bluetoothRouteManager.isWatch(device);
+ // 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, null);
+ }
+
+ 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, "onDestRouteAsPendingRoute: route=%s, "
+ + "AudioManager#setCommunicationDevice()=%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, BluetoothRouteManager bluetoothRouteManager) {
+ Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%s)", active,
+ DEVICE_TYPE_STRINGS.get(mAudioRouteType));
+ if (active) {
+ int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
+ audioManager);
+ if (mAudioRouteType == TYPE_SPEAKER) {
+ pendingAudioRoute.addMessage(SPEAKER_OFF, null);
+ } else if (mAudioRouteType == TYPE_BLUETOOTH_SCO
+ && result == BluetoothStatusCodes.SUCCESS) {
+ // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
+ pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public AudioRoute(@AudioRouteType int type, String bluetoothAddress, AudioDeviceInfo info) {
+ mAudioRouteType = type;
+ mBluetoothAddress = bluetoothAddress;
+ mInfo = info;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof AudioRoute otherRoute)) {
+ return false;
+ }
+ if (mAudioRouteType != otherRoute.getType()) {
+ return false;
+ }
+ return !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) || mBluetoothAddress.equals(
+ otherRoute.getBluetoothAddress());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAudioRouteType, mBluetoothAddress);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[Type=" + DEVICE_TYPE_STRINGS.get(mAudioRouteType)
+ + ", 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, "clearCommunicationDevice: Disconnecting SCO device.");
+ result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+ } else {
+ Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice, type=%s",
+ DEVICE_TYPE_STRINGS.get(pendingAudioRoute.getCommunicationDeviceType()));
+ audioManager.clearCommunicationDevice();
+ }
+
+ if (result == BluetoothStatusCodes.SUCCESS) {
+ if (pendingAudioRoute.getFeatureFlags().resolveActiveBtRoutingAndBtTimingIssue()) {
+ maybeClearConnectedPendingMessages(pendingAudioRoute);
+ }
+ pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+ }
+ return result;
+ }
+
+ private void maybeClearConnectedPendingMessages(PendingAudioRoute pendingAudioRoute) {
+ // If we're still waiting on BT_AUDIO_CONNECTED/SPEAKER_ON but have routed out of it
+ // since and disconnected the device, then remove that message so we aren't waiting for
+ // it in the message queue.
+ if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+ Log.i(this, "clearCommunicationDevice: Clearing pending "
+ + "BT_AUDIO_CONNECTED messages.");
+ pendingAudioRoute.clearPendingMessage(
+ new Pair<>(BT_AUDIO_CONNECTED, mBluetoothAddress));
+ } else if (mAudioRouteType == TYPE_SPEAKER) {
+ Log.i(this, "clearCommunicationDevice: Clearing pending SPEAKER_ON messages.");
+ pendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/CachedAvailableEndpointsChange.java b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
new file mode 100644
index 0000000..fc98991
--- /dev/null
+++ b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
@@ -0,0 +1,75 @@
+/*
+ * 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 int getCacheType() {
+ return TYPE_STATE;
+ }
+
+ @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/CachedCallEventQueue.java b/src/com/android/server/telecom/CachedCallEventQueue.java
new file mode 100644
index 0000000..9ce51bf
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCallEventQueue.java
@@ -0,0 +1,48 @@
+/*
+ * 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.os.Bundle;
+import android.telecom.Log;
+
+public class CachedCallEventQueue implements CachedCallback {
+ public static final String ID = CachedCallEventQueue.class.getSimpleName();
+
+ private final String mEvent;
+ private final Bundle mExtras;
+
+ public CachedCallEventQueue(String event, Bundle extras) {
+ mEvent = event;
+ mExtras = extras;
+ }
+
+ @Override
+ public int getCacheType() {
+ return TYPE_QUEUE;
+ }
+
+ @Override
+ public void executeCallback(CallSourceService service, Call call) {
+ Log.addEvent(call, LogUtils.Events.CALL_EVENT, mEvent);
+ service.sendCallEvent(call, mEvent, mExtras);
+ }
+
+ @Override
+ public String getCallbackId() {
+ return ID;
+ }
+}
diff --git a/src/com/android/server/telecom/CachedCallback.java b/src/com/android/server/telecom/CachedCallback.java
new file mode 100644
index 0000000..c354beb
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCallback.java
@@ -0,0 +1,72 @@
+/*
+ * 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 callback is caching a state, meaning any new CachedCallbacks with the same
+ * {@link #getCallbackId()} will REPLACE any existing CachedCallback.
+ */
+ int TYPE_STATE = 0;
+ /**
+ * This callback is caching a Queue, meaning that any new CachedCallbacks with the same
+ * {@link #getCallbackId()} will enqueue as a FIFO queue and each instance of this
+ * CachedCallback will run {@link #executeCallback(CallSourceService, Call)}.
+ */
+ int TYPE_QUEUE = 1;
+
+ /**
+ * This method allows the callback to determine whether it is caching a {@link #TYPE_STATE} or
+ * a {@link #TYPE_QUEUE}.
+ *
+ * @return Either {@link #TYPE_STATE} or {@link #TYPE_QUEUE} based on the callback type.
+ */
+ int getCacheType();
+
+ /**
+ * 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);
+
+ /**
+ * The ID that this CachedCallback should use to identify itself as a distinct operation.
+ * <p>
+ * If {@link #TYPE_STATE} is set for {@link #getCacheType()}, and a CachedCallback with the
+ * same ID is called multiple times while the service is not set, ONLY the last callback will be
+ * sent to the client since the last callback is the most relevant.
+ * <p>
+ * If {@link #TYPE_QUEUE} is set for {@link #getCacheType()} and the CachedCallback with the
+ * same ID is called multiple times while the service is not set, each CachedCallback will be
+ * enqueued in FIFO order. Once the service is set, {@link #executeCallback} will be called
+ * for each CachedCallback with the same ID.
+ *
+ * @return A unique callback id that will be used differentiate this CachedCallback type with
+ * other CachedCallback types.
+ */
+ 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..1d838f0
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
@@ -0,0 +1,66 @@
+/*
+ * 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 int getCacheType() {
+ return TYPE_STATE;
+ }
+
+ @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..ee1227b
--- /dev/null
+++ b/src/com/android/server/telecom/CachedMuteStateChange.java
@@ -0,0 +1,62 @@
+/*
+ * 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 int getCacheType() {
+ return TYPE_STATE;
+ }
+
+ @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..8aa6d40
--- /dev/null
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -0,0 +1,69 @@
+/*
+ * 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.callsequencing.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 int getCacheType() {
+ return TYPE_STATE;
+ }
+
+ @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 951865b..df31e02 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -17,24 +17,30 @@
package com.android.server.telecom;
import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
-import static android.telecom.Call.EVENT_DISPLAY_SOS_MESSAGE;
+import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
+
+import static com.android.server.telecom.CachedCallback.TYPE_QUEUE;
+import static com.android.server.telecom.CachedCallback.TYPE_STATE;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+ .TransactionalVideoStateToString;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+ .VideoProfileStateToTransactionalVideoState;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
-import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.os.Trace;
import android.os.UserHandle;
import android.provider.CallLog;
import android.provider.ContactsContract.Contacts;
@@ -43,6 +49,7 @@
import android.telecom.CallAudioState;
import android.telecom.CallDiagnosticService;
import android.telecom.CallDiagnostics;
+import android.telecom.CallException;
import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
@@ -55,7 +62,6 @@
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
-import android.telecom.Response;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
@@ -70,9 +76,13 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IVideoProvider;
import com.android.internal.util.Preconditions;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.stats.CallStateChangedAtomWriter;
import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.io.IOException;
import java.text.SimpleDateFormat;
@@ -80,6 +90,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;
@@ -88,6 +99,7 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -118,6 +130,24 @@
private static final char NO_DTMF_TONE = '\0';
+
+ /**
+ * Listener for CallState changes which can be leveraged by a Transaction.
+ */
+ public interface CallStateListener {
+ void onCallStateChanged(int newCallState);
+ }
+
+ public List<CallStateListener> mCallStateListeners = new ArrayList<>();
+
+ public void addCallStateListener(CallStateListener newListener) {
+ mCallStateListeners.add(newListener);
+ }
+
+ public boolean removeCallStateListener(CallStateListener newListener) {
+ return mCallStateListeners.remove(newListener);
+ }
+
/**
* Listener for events on the call.
*/
@@ -283,18 +313,25 @@
@Override
public void onCallerInfoQueryComplete(Uri handle, CallerInfo callerInfo) {
synchronized (mLock) {
- Call.this.setCallerInfo(handle, callerInfo);
+ Call call = Call.this;
+ if (call != null) {
+ call.setCallerInfo(handle, callerInfo);
+ }
}
}
@Override
public void onContactPhotoQueryComplete(Uri handle, CallerInfo callerInfo) {
synchronized (mLock) {
- Call.this.setCallerInfo(handle, callerInfo);
+ Call call = Call.this;
+ if (call != null) {
+ call.setCallerInfo(handle, callerInfo);
+ }
}
}
};
+ private final boolean mIsModifyStatePermissionGranted;
/**
* One of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING, or CALL_DIRECTION_UNKNOWN
*/
@@ -622,6 +659,36 @@
private boolean mIsVideoCallingSupportedByPhoneAccount = false;
/**
+ * Indicates whether this individual calls video state can be changed as opposed to be gated
+ * by the {@link PhoneAccount}.
+ *
+ * {@code True} if the call is Transactional && has the CallAttributes.SUPPORTS_VIDEO_CALLING
+ * capability {@code false} otherwise.
+ */
+ private boolean mTransactionalCallSupportsVideoCalling = false;
+
+ public void setTransactionalCallSupportsVideoCalling(CallAttributes callAttributes) {
+ if (!mIsTransactionalCall) {
+ Log.i(this, "setTransactionalCallSupportsVideoCalling: call is not transactional");
+ return;
+ }
+ if (callAttributes == null) {
+ Log.i(this, "setTransactionalCallSupportsVideoCalling: callAttributes is null");
+ return;
+ }
+ if ((callAttributes.getCallCapabilities() & CallAttributes.SUPPORTS_VIDEO_CALLING)
+ == CallAttributes.SUPPORTS_VIDEO_CALLING) {
+ mTransactionalCallSupportsVideoCalling = true;
+ } else {
+ mTransactionalCallSupportsVideoCalling = false;
+ }
+ }
+
+ public boolean isTransactionalCallSupportsVideoCalling() {
+ return mTransactionalCallSupportsVideoCalling;
+ }
+
+ /**
* Indicates whether or not this call can be pulled if it is an external call. If true, respect
* the Connection Capability set by the ConnectionService. If false, override the capability
* set and always remove the ability to pull this external call.
@@ -762,7 +829,78 @@
* 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
+ * corresponding bluetooth {@link android.telecom.InCallService} is successfully bound or timed
+ * out.
+ */
+ private CompletableFuture<Boolean> mBtIcsFuture;
+
+ /**
+ * Map of CachedCallbacks that are pending to be executed when the *ServiceWrapper connects
+ */
+ private final Map<String, List<CachedCallback>> mCachedServiceCallbacks = new HashMap<>();
+
+ public void cacheServiceCallback(CachedCallback callback) {
+ synchronized (mCachedServiceCallbacks) {
+ if (mFlags.cacheCallEvents()) {
+ // If there are multiple threads caching + calling processCachedCallbacks at the
+ // same time, there is a race - double check here to ensure that we do not lose an
+ // operation due to a a cache happening after processCachedCallbacks.
+ // Either service will be non-null in this case, but both will not be non-null
+ if (mConnectionService != null) {
+ callback.executeCallback(mConnectionService, this);
+ return;
+ }
+ if (mTransactionalService != null) {
+ callback.executeCallback(mTransactionalService, this);
+ return;
+ }
+ }
+ List<CachedCallback> cbs = mCachedServiceCallbacks.computeIfAbsent(
+ callback.getCallbackId(), k -> new ArrayList<>());
+ switch (callback.getCacheType()) {
+ case TYPE_STATE: {
+ cbs.clear();
+ cbs.add(callback);
+ break;
+ }
+ case TYPE_QUEUE: {
+ cbs.add(callback);
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public Map<String, List<CachedCallback>> getCachedServiceCallbacksCopy() {
+ synchronized (mCachedServiceCallbacks) {
+ // This should only be used during testing, but to be safe, since there is internally a
+ // List value, we need to do a deep copy to ensure someone with a ref to the Map doesn't
+ // mutate the underlying list while we are modifying it in cacheServiceCallback.
+ return mCachedServiceCallbacks.entrySet().stream().collect(
+ Collectors.toUnmodifiableMap(Map.Entry::getKey, e-> List.copyOf(e.getValue())));
+ }
+ }
+
+ private FeatureFlags mFlags;
/**
* Persists the specified parameters and initializes the new instance.
@@ -795,11 +933,12 @@
boolean shouldAttachToExistingConnection,
boolean isConference,
ClockProxy clockProxy,
- ToastFactory toastFactory) {
+ ToastFactory toastFactory,
+ FeatureFlags featureFlags) {
this(callId, context, callsManager, lock, repository, phoneNumberUtilsAdapter,
handle, null, gatewayInfo, connectionManagerPhoneAccountHandle,
targetPhoneAccountHandle, callDirection, shouldAttachToExistingConnection,
- isConference, clockProxy, toastFactory);
+ isConference, clockProxy, toastFactory, featureFlags);
}
@@ -819,8 +958,9 @@
boolean shouldAttachToExistingConnection,
boolean isConference,
ClockProxy clockProxy,
- ToastFactory toastFactory) {
-
+ ToastFactory toastFactory,
+ FeatureFlags featureFlags) {
+ mFlags = featureFlags;
mId = callId;
mConnectionId = callId;
mState = (isConference && callDirection != CALL_DIRECTION_INCOMING &&
@@ -831,7 +971,6 @@
mLock = lock;
mRepository = repository;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
- setHandle(handle);
mParticipants = participants;
mPostDialDigits = handle != null
? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
@@ -839,6 +978,7 @@
setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
mCallDirection = callDirection;
setTargetPhoneAccount(targetPhoneAccountHandle);
+ setHandle(handle);
mIsConference = isConference;
mShouldAttachToExistingConnection = shouldAttachToExistingConnection
|| callDirection == CALL_DIRECTION_INCOMING;
@@ -851,6 +991,8 @@
mStartRingTime = 0;
mCallStateChangedAtomWriter.setExistingCallCount(callsManager.getCalls().size());
+ mIsModifyStatePermissionGranted =
+ isModifyPhoneStatePermissionGranted(getDelegatePhoneAccountHandle());
}
/**
@@ -870,6 +1012,7 @@
* connection, regardless of whether it's incoming or outgoing.
* @param connectTimeMillis The connection time of the call.
* @param clockProxy
+ * @param featureFlags The telecom feature flags.
*/
Call(
String callId,
@@ -888,11 +1031,13 @@
long connectTimeMillis,
long connectElapsedTimeMillis,
ClockProxy clockProxy,
- ToastFactory toastFactory) {
+ ToastFactory toastFactory,
+ FeatureFlags featureFlags) {
this(callId, context, callsManager, lock, repository,
phoneNumberUtilsAdapter, handle, gatewayInfo,
connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection,
- shouldAttachToExistingConnection, isConference, clockProxy, toastFactory);
+ shouldAttachToExistingConnection, isConference, clockProxy, toastFactory,
+ featureFlags);
mConnectTimeMillis = connectTimeMillis;
mConnectElapsedTimeMillis = connectElapsedTimeMillis;
@@ -1128,7 +1273,6 @@
return (!mIsTransactionalCall ? mConnectionService : mTransactionalService);
}
- @VisibleForTesting
public int getState() {
return mState;
}
@@ -1227,7 +1371,7 @@
message, null));
}
- mDisconnectFuture.complete(true);
+ mDiagnosticCompleteFuture.complete(true);
} else {
Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
getId());
@@ -1249,6 +1393,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) {
@@ -1336,6 +1486,12 @@
Log.addEvent(this, event, stringData);
}
+ if (mFlags.transactionalCsVerifier()) {
+ for (CallStateListener listener : mCallStateListeners) {
+ listener.onCallStateChanged(newState);
+ }
+ }
+
mCallStateChangedAtomWriter
.setDisconnectCause(getDisconnectCause())
.setSelfManaged(isSelfManaged())
@@ -1479,6 +1635,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;
@@ -1492,7 +1651,13 @@
mIsTestEmergencyCall = mHandle != null &&
isTestEmergencyCall(mHandle.getSchemeSpecificPart());
}
- startCallerInfoLookup();
+ if (mTargetPhoneAccountHandle == null || !mContext.getResources().getString(
+ R.string.skip_incoming_caller_info_account_package).equalsIgnoreCase(
+ mTargetPhoneAccountHandle.getComponentName().getPackageName())) {
+ startCallerInfoLookup();
+ } else {
+ Log.i(this, "skip incoming caller info lookup");
+ }
for (Listener l : mListeners) {
l.onHandleChanged(this);
}
@@ -1507,6 +1672,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) {
@@ -1741,8 +1909,12 @@
accountHandle.getComponentName().getPackageName(),
mContext.getPackageManager());
// Set the associated user for the call for MT calls based on the target phone account.
- if (isIncoming() && !accountHandle.getUserHandle().equals(mAssociatedUser)) {
- setAssociatedUser(accountHandle.getUserHandle());
+ UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+ mFlags.associatedUserRefactorForWorkProfile(),
+ mCallsManager.getPhoneAccountRegistrar(), mCallsManager.getCurrentUserHandle(),
+ accountHandle);
+ if (isIncoming() && !associatedUser.equals(mAssociatedUser)) {
+ setAssociatedUser(associatedUser);
}
}
}
@@ -1810,7 +1982,6 @@
PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, false);
}
- @VisibleForTesting
public boolean isIncoming() {
return mCallDirection == CALL_DIRECTION_INCOMING;
}
@@ -1914,7 +2085,29 @@
}
public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
+ Log.i(this, "setTransactionServiceWrapper: service=[%s]", service);
mTransactionalService = service;
+ processCachedCallbacks(service);
+ }
+
+ private void processCachedCallbacks(CallSourceService service) {
+ if(mFlags.cacheCallAudioCallbacks()) {
+ synchronized (mCachedServiceCallbacks) {
+ for (List<CachedCallback> callbacks : mCachedServiceCallbacks.values()) {
+ callbacks.forEach( callback -> 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() {
@@ -1996,13 +2189,18 @@
userHandle = mTargetPhoneAccountHandle.getUserHandle();
}
if (userHandle != null) {
- isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
+ isWorkCall = UserUtil.isManagedProfile(mContext, userHandle, mFlags);
}
- isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
- PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) && phoneAccount.getExtras() != null
- && phoneAccount.getExtras().getBoolean(
- PhoneAccount.EXTRA_PLAY_CALL_RECORDING_TONE, false));
+ if (!mFlags.telecomResolveHiddenDependencies()) {
+ isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ && phoneAccount.getExtras() != null
+ && phoneAccount.getExtras().getBoolean(
+ PhoneAccount.EXTRA_PLAY_CALL_RECORDING_TONE, false));
+ } else {
+ isCallRecordingToneSupported = false;
+ }
isSimCall = phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
}
mIsWorkCall = isWorkCall;
@@ -2075,7 +2273,6 @@
* @return The "age" of this call object in milliseconds, which typically also represents the
* period since this call was added to the set pending outgoing calls.
*/
- @VisibleForTesting
public long getAgeMillis() {
if (mState == CallState.DISCONNECTED &&
(mDisconnectCause.getCode() == DisconnectCause.REJECTED ||
@@ -2134,6 +2331,25 @@
setConnectionCapabilities(connectionCapabilities, false /* forceUpdate */);
}
+ public void setTransactionalCapabilities(Bundle extras) {
+ if (!mFlags.remapTransactionalCapabilities()) {
+ setConnectionCapabilities(
+ extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
+ CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ return;
+ }
+ int connectionCapabilitesBitmap = 0;
+ int transactionalCapabilitiesBitmap = extras.getInt(
+ CallAttributes.CALL_CAPABILITIES_KEY,
+ CallAttributes.SUPPORTS_SET_INACTIVE);
+ if ((transactionalCapabilitiesBitmap & CallAttributes.SUPPORTS_SET_INACTIVE)
+ == CallAttributes.SUPPORTS_SET_INACTIVE) {
+ connectionCapabilitesBitmap = connectionCapabilitesBitmap | Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD;
+ }
+ setConnectionCapabilities(connectionCapabilitesBitmap, true);
+ }
+
void setConnectionCapabilities(int connectionCapabilities, boolean forceUpdate) {
Log.v(this, "setConnectionCapabilities: %s", Connection.capabilitiesToString(
connectionCapabilities));
@@ -2321,6 +2537,7 @@
@VisibleForTesting
public void setConnectionService(ConnectionServiceWrapper service) {
+ Log.i(this, "setConnectionService: service=[%s]", service);
setConnectionService(service, null);
}
@@ -2335,7 +2552,7 @@
service.incrementAssociatedCallCount();
- if (remoteService != null) {
+ if (mFlags.updatedRcsCallCountTracking() && remoteService != null) {
remoteService.incrementAssociatedCallCount();
mRemoteConnectionService = remoteService;
}
@@ -2343,6 +2560,7 @@
mConnectionService = service;
mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
mConnectionService.addCall(this);
+ processCachedCallbacks(service);
}
/**
@@ -2366,11 +2584,17 @@
if (mConnectionService != null) {
ConnectionServiceWrapper serviceTemp = mConnectionService;
- // Continue to track the former CS for this call so that it doesn't unbind early:
- mRemoteConnectionService = serviceTemp;
+ if (mFlags.updatedRcsCallCountTracking()) {
+ // Continue to track the former CS for this call so that it doesn't unbind early:
+ mRemoteConnectionService = serviceTemp;
+ }
mConnectionService = null;
serviceTemp.removeCall(this);
+
+ if (!mFlags.updatedRcsCallCountTracking()) {
+ serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
+ }
}
service.incrementAssociatedCallCount();
@@ -2397,7 +2621,7 @@
// to do.
decrementAssociatedCallCount(serviceTemp);
- if (remoteServiceTemp != null) {
+ if (mFlags.updatedRcsCallCountTracking() && remoteServiceTemp != null) {
decrementAssociatedCallCount(remoteServiceTemp);
}
}
@@ -2418,7 +2642,7 @@
return;
}
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
- phoneAccountRegistrar, mContext);
+ phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
mCreateConnectionProcessor.process();
}
@@ -2931,11 +3155,19 @@
hold(null /* reason */);
}
+ /**
+ * This method requests the ConnectionService or TransactionalService hosting the call to put
+ * the call on hold
+ */
public void hold(String reason) {
if (mState == CallState.ACTIVE) {
if (mTransactionalService != null) {
mTransactionalService.onSetInactive(this);
} else if (mConnectionService != null) {
+ if (mFlags.transactionalCsVerifier()) {
+ awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(),
+ "hold");
+ }
mConnectionService.hold(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2946,6 +3178,35 @@
}
/**
+ * helper that can be used for any callback that requests a call state change and wants to
+ * verify the change
+ */
+ public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
+ boolean shouldDisconnectUponTimeout, String callingMethod) {
+ TransactionManager tm = TransactionManager.getInstance();
+ tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
+ this, targetCallState), new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult 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);
+ }
+ }
+ });
+ }
+
+ /**
* Releases the call from hold if it is currently active.
*/
@VisibleForTesting
@@ -3072,6 +3333,12 @@
Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE));
}
+ if (mExtras.containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) {
+ if (source != SOURCE_CONNECTION_SERVICE || !mIsModifyStatePermissionGranted) {
+ mExtras.remove(TelecomManager.EXTRA_DO_NOT_LOG_CALL);
+ }
+ }
+
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
Log.addEvent(this, LogUtils.Events.ICS_EXTRAS_CHANGED);
@@ -3080,12 +3347,20 @@
} else if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
- Log.e(this, new NullPointerException(),
- "putExtras failed due to null CS callId=%s", getId());
+ Log.w(this, "putExtras failed due to null CS callId=%s", getId());
}
}
}
+ private boolean isModifyPhoneStatePermissionGranted(PhoneAccountHandle phoneAccountHandle) {
+ if (phoneAccountHandle == null) {
+ return false;
+ }
+ String packageName = phoneAccountHandle.getComponentName().getPackageName();
+ return PackageManager.PERMISSION_GRANTED == mContext.getPackageManager().checkPermission(
+ android.Manifest.permission.MODIFY_PHONE_STATE, packageName);
+ }
+
/**
* Removes extras from the extras bundle associated with this {@link Call}.
*
@@ -3296,7 +3571,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;
}
@@ -3305,97 +3580,43 @@
}
/**
- * Sends a call event to the {@link ConnectionService} for this call. This function is
- * called for event other than {@link Call#EVENT_REQUEST_HANDOVER}
+ * Sends a call event to the {@link ConnectionService} for this call.
*
* @param event The call event.
* @param extras Associated extras.
*/
public void sendCallEvent(String event, Bundle extras) {
- sendCallEvent(event, 0/*For Event != EVENT_REQUEST_HANDOVER*/, extras);
- }
-
- /**
- * Sends a call event to the {@link ConnectionService} for this call.
- *
- * See {@link Call#sendCallEvent(String, Bundle)}.
- *
- * @param event The call event.
- * @param targetSdkVer SDK version of the app calling this api
- * @param extras Associated extras.
- */
- public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
if (mConnectionService != null || mTransactionalService != null) {
- if (android.telecom.Call.EVENT_REQUEST_HANDOVER.equals(event)) {
- if (targetSdkVer > Build.VERSION_CODES.P) {
- Log.e(this, new Exception(), "sendCallEvent failed. Use public api handoverTo" +
- " for API > 28(P)");
- // Event-based Handover APIs are deprecated, so inform the user.
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mToastFactory.makeText(mContext,
- "WARNING: Event-based handover APIs are deprecated and will no"
- + " longer function in Android Q.",
- Toast.LENGTH_LONG).show();
- }
- });
-
- // Uncomment and remove toast at feature complete: return;
- }
-
- // Handover requests are targeted at Telecom, not the ConnectionService.
- if (extras == null) {
- Log.w(this, "sendCallEvent: %s event received with null extras.",
- android.telecom.Call.EVENT_REQUEST_HANDOVER);
- sendEventToService(this, android.telecom.Call.EVENT_HANDOVER_FAILED,
- null);
- return;
- }
- Parcelable parcelable = extras.getParcelable(
- android.telecom.Call.EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE);
- if (!(parcelable instanceof PhoneAccountHandle) || parcelable == null) {
- Log.w(this, "sendCallEvent: %s event received with invalid handover acct.",
- android.telecom.Call.EVENT_REQUEST_HANDOVER);
- sendEventToService(this, android.telecom.Call.EVENT_HANDOVER_FAILED, null);
- return;
- }
- PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) parcelable;
- int videoState = extras.getInt(android.telecom.Call.EXTRA_HANDOVER_VIDEO_STATE,
- VideoProfile.STATE_AUDIO_ONLY);
- Parcelable handoverExtras = extras.getParcelable(
- android.telecom.Call.EXTRA_HANDOVER_EXTRAS);
- Bundle handoverExtrasBundle = null;
- if (handoverExtras instanceof Bundle) {
- handoverExtrasBundle = (Bundle) handoverExtras;
- }
- requestHandover(phoneAccountHandle, videoState, handoverExtrasBundle, true);
- } else {
- // Relay bluetooth call quality reports to the call diagnostic service.
- if (BluetoothCallQualityReport.EVENT_BLUETOOTH_CALL_QUALITY_REPORT.equals(event)
- && extras.containsKey(
- BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT)) {
- notifyBluetoothCallQualityReport(extras.getParcelable(
- BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT
- ));
- }
- Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
- sendEventToService(this, event, extras);
+ // Relay bluetooth call quality reports to the call diagnostic service.
+ if (BluetoothCallQualityReport.EVENT_BLUETOOTH_CALL_QUALITY_REPORT.equals(event)
+ && extras.containsKey(
+ BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT)) {
+ notifyBluetoothCallQualityReport(extras.getParcelable(
+ BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT
+ ));
}
+ Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
+ sendEventToService(this, event, extras);
} else {
- Log.e(this, new NullPointerException(),
- "sendCallEvent failed due to null CS callId=%s", getId());
+ if (mFlags.cacheCallEvents()) {
+ Log.i(this, "sendCallEvent: caching call event for callId=%s, event=%s",
+ getId(), event);
+ cacheServiceCallback(new CachedCallEventQueue(event, extras));
+ } else {
+ Log.e(this, new NullPointerException(),
+ "sendCallEvent failed due to null CS callId=%s", getId());
+ }
}
}
/**
- * This method should only be called from sendCallEvent(String, int, Bundle).
+ * This method should only be called from sendCallEvent(String, Bundle).
*/
private void sendEventToService(Call call, String event, Bundle extras) {
if (mConnectionService != null) {
mConnectionService.sendCallEvent(call, event, extras);
} else if (mTransactionalService != null) {
- mTransactionalService.onEvent(call, event, extras);
+ mTransactionalService.sendCallEvent(call, event, extras);
}
}
@@ -3589,6 +3810,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;
}
@@ -3609,8 +3835,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;
}
@@ -3684,7 +3914,6 @@
* @param callerInfo The new caller information to set.
*/
private void setCallerInfo(Uri handle, CallerInfo callerInfo) {
- Trace.beginSection("setCallerInfo");
if (callerInfo == null) {
Log.i(this, "CallerInfo lookup returned null, skipping update");
return;
@@ -3696,7 +3925,8 @@
}
String newName = callerInfo.getName();
- boolean contactNameChanged = mCallerInfo == null || !mCallerInfo.getName().equals(newName);
+ boolean contactNameChanged = mCallerInfo == null ||
+ !Objects.equals(mCallerInfo.getName(), newName);
mCallerInfo = callerInfo;
Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
@@ -3707,8 +3937,6 @@
l.onCallerInfoChanged(this);
}
}
-
- Trace.endSection();
}
public CallerInfo getCallerInfo() {
@@ -3722,7 +3950,7 @@
Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
mCannedSmsResponsesLoadingStarted = true;
mCallsManager.getRespondViaSmsManager().loadCannedTextMessages(
- new Response<Void, List<String>>() {
+ new CallsManager.Response<Void, List<String>>() {
@Override
public void onResult(Void request, List<String>... result) {
if (result.length > 0) {
@@ -3963,6 +4191,9 @@
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
// not include the video state history when:
@@ -3978,13 +4209,26 @@
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) {
+ int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
+ 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)) {
mAnalytics.setCallIsVideo(true);
}
@@ -4065,7 +4309,7 @@
* @param associatedUser
*/
public void setAssociatedUser(UserHandle associatedUser) {
- Log.i(this, "Setting associated user for call");
+ Log.i(this, "Setting associated user for call: %s", associatedUser);
Preconditions.checkNotNull(associatedUser);
mAssociatedUser = associatedUser;
}
@@ -4215,8 +4459,8 @@
l.onReceivedCallQualityReport(this, callQuality);
}
} else {
- if (event.equals(EVENT_DISPLAY_SOS_MESSAGE) && !isEmergencyCall()) {
- Log.w(this, "onConnectionEvent: EVENT_DISPLAY_SOS_MESSAGE is sent "
+ if (event.equals(EVENT_DISPLAY_EMERGENCY_MESSAGE) && !isEmergencyCall()) {
+ Log.w(this, "onConnectionEvent: EVENT_DISPLAY_EMERGENCY_MESSAGE is sent "
+ "without an emergency call");
return;
}
@@ -4537,6 +4781,7 @@
}
public void setStartFailCause(CallFailureCause cause) {
+ Log.i(this, "setStartFailCause: cause = %s; callId = %s", cause, this.getId());
mCallStateChangedAtomWriter.setStartFailCause(cause);
}
@@ -4589,17 +4834,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) -> {
@@ -4607,14 +4852,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;
}
/**
@@ -4622,7 +4867,7 @@
* if this is handled immediately.
*/
public boolean isDisconnectHandledViaFuture() {
- return mDisconnectFuture != null;
+ return mDiagnosticCompleteFuture != null;
}
/**
@@ -4630,9 +4875,70 @@
* {@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.
+ * @param btIcsFuture the {@link CompletableFuture}
+ */
+ public void setBtIcsFuture(CompletableFuture<Boolean> btIcsFuture) {
+ mBtIcsFuture = btIcsFuture;
+ }
+
+ /**
+ * @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/CallAnomalyWatchdog.java b/src/com/android/server/telecom/CallAnomalyWatchdog.java
index 045671e..c331b29 100644
--- a/src/com/android/server/telecom/CallAnomalyWatchdog.java
+++ b/src/com/android/server/telecom/CallAnomalyWatchdog.java
@@ -18,15 +18,23 @@
import static com.android.server.telecom.LogUtils.Events.STATE_TIMEOUT;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
import android.util.LocalLog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.stats.CallStateChangedAtomWriter;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.Collections;
import java.util.Map;
@@ -113,6 +121,7 @@
private final TelecomSystem.SyncRoot mLock;
private final Timeouts.Adapter mTimeoutAdapter;
private final ClockProxy mClockProxy;
+ private final FeatureFlags mFeatureFlags;
private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
// Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
@@ -122,6 +131,7 @@
private final Set<Call> mCallsPendingDestruction = Collections.newSetFromMap(
new ConcurrentHashMap<>(2));
private final LocalLog mLocalLog = new LocalLog(20);
+ private final TelecomMetricsController mMetricsController;
/**
* Enables the action to disconnect the call when the Transitory state and Intermediate state
@@ -140,6 +150,12 @@
UUID.fromString("d57d8aab-d723-485e-a0dd-d1abb0f346c8");
public static final String WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_MSG =
"Telecom CallAnomalyWatchdog caught and disconnected a stuck/zombie emergency call.";
+ public static final UUID WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_UUID =
+ UUID.fromString("3fbecd12-059d-4fd3-87b7-6c3079891c23");
+ public static final String WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_MSG =
+ "A VoIP call was flagged due to exceeding a one-minute threshold in the DIALING or "
+ + "RINGING state";
+
@VisibleForTesting
public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
@@ -148,13 +164,17 @@
public CallAnomalyWatchdog(ScheduledExecutorService executorService,
TelecomSystem.SyncRoot lock,
+ FeatureFlags featureFlags,
Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy,
- EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
+ EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
+ TelecomMetricsController metricsController) {
mScheduledExecutorService = executorService;
mLock = lock;
+ mFeatureFlags = featureFlags;
mTimeoutAdapter = timeoutAdapter;
mClockProxy = clockProxy;
mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
+ mMetricsController = metricsController;
}
/**
@@ -170,6 +190,9 @@
@Override
public void onCallAdded(Call call) {
maybeTrackCall(call);
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getCallStats().onCallStart(call);
+ }
}
/**
@@ -191,6 +214,9 @@
public void onCallRemoved(Call call) {
Log.i(this, "onCallRemoved: call=%s", call.toString());
stopTrackingCall(call);
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getCallStats().onCallEnd(call);
+ }
}
/**
@@ -272,8 +298,13 @@
*/
private void maybeTrackCall(Call call) {
final WatchdogCallState currentState = mWatchdogCallStateMap.get(call);
+ boolean isCreateConnectionComplete = call.isCreateConnectionComplete();
+ if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) {
+ isCreateConnectionComplete =
+ isCreateConnectionComplete || call.isTransactionalCall();
+ }
final WatchdogCallState newState = new WatchdogCallState(call.getState(),
- call.isCreateConnectionComplete(), mClockProxy.elapsedRealtime());
+ isCreateConnectionComplete, mClockProxy.elapsedRealtime());
if (Objects.equals(currentState, newState)) {
// No state change; skip.
return;
@@ -348,8 +379,13 @@
}
// Ensure that at timeout we are still in the original state when we posted the
// timeout.
+ boolean isCreateConnectionComplete = call.isCreateConnectionComplete();
+ if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) {
+ isCreateConnectionComplete =
+ isCreateConnectionComplete || call.isTransactionalCall();
+ }
final WatchdogCallState expiredState = new WatchdogCallState(call.getState(),
- call.isCreateConnectionComplete(), mClockProxy.elapsedRealtime());
+ isCreateConnectionComplete, mClockProxy.elapsedRealtime());
if (expiredState.equals(newState)
&& getDurationInCurrentStateMillis(newState) > timeoutMillis) {
// The call has been in this transitory or intermediate state too long,
@@ -368,7 +404,7 @@
WATCHDOG_DISCONNECTED_STUCK_CALL_MSG);
}
- if (isEnabledDisconnect) {
+ if (isEnabledDisconnect || isInSelfManagedStuckStartingState(call)) {
call.setOverrideDisconnectCauseCode(
new DisconnectCause(DisconnectCause.ERROR, "state_timeout"));
call.disconnect("State timeout");
@@ -387,6 +423,50 @@
return cleanupRunnable;
}
+ private boolean isInSelfManagedStuckStartingState(Call call) {
+ Context context = call.getContext();
+ if (!mFeatureFlags.disconnectSelfManagedStuckStartupCalls() || context == null) {
+ return false;
+ }
+ int currentStuckState = call.getState();
+ return call.isSelfManaged() &&
+ (currentStuckState == CallState.NEW ||
+ currentStuckState == CallState.RINGING ||
+ currentStuckState == CallState.DIALING ||
+ currentStuckState == CallState.CONNECTING) &&
+ isVanillaIceCreamBuildOrHigher(context, call);
+ }
+
+ private boolean isVanillaIceCreamBuildOrHigher(Context context, Call call) {
+ // report the anomaly for metrics purposes
+ mAnomalyReporter.reportAnomaly(
+ WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_UUID,
+ WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_MSG);
+ // only disconnect calls running on V and when the flag is enabled!
+ PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+ PackageManager pm = context.getPackageManager();
+ if (pm == null ||
+ phoneAccountHandle == null ||
+ phoneAccountHandle.getComponentName() == null) {
+ return false;
+ }
+ String packageName = phoneAccountHandle.getComponentName().getPackageName();
+ Log.d(this, "pah=[%s], user=[%s]", phoneAccountHandle, call.getAssociatedUser());
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = pm.getApplicationInfoAsUser(
+ packageName,
+ 0,
+ call.getAssociatedUser());
+ } catch (Exception e) {
+ Log.e(this, e, "iVICBOH: pm.getApplicationInfoAsUser(...) exception");
+ return false;
+ }
+ int targetSdk = (applicationInfo == null) ? 0 : applicationInfo.targetSdkVersion;
+ Log.i(this, "iVICBOH: packageName=[%s], sdk=[%d]", packageName, targetSdk);
+ return targetSdk >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
+ }
+
/**
* Returns whether the action to disconnect the call when the Transitory state and
* Intermediate state time expires is enabled or disabled.
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
new file mode 100644
index 0000000..8d5f9fd
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.AudioRoute.BT_AUDIO_DEVICE_INFO_TYPES;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.Flags;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.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
+ * use cases. Handles the set/clear communication use case logic for all audio routes (speaker, BT,
+ * headset, and earpiece). For BT devices, this handles switches between hearing aids, SCO, and LE
+ * audio (also takes into account switching between multiple LE audio devices).
+ */
+public class CallAudioCommunicationDeviceTracker {
+
+ // Use -1 indicates device is not set for any communication use case
+ private static final int sAUDIO_DEVICE_TYPE_INVALID = -1;
+ private AudioManager mAudioManager;
+ private BluetoothRouteManager mBluetoothRouteManager;
+ private @AudioDeviceInfo.AudioDeviceType int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
+ // Keep track of the locally requested BT audio device if set
+ private String mBtAudioDevice = null;
+ private final Lock mLock = new ReentrantLock();
+
+ public CallAudioCommunicationDeviceTracker(Context context) {
+ mAudioManager = context.getSystemService(AudioManager.class);
+ }
+
+ public void setBluetoothRouteManager(BluetoothRouteManager bluetoothRouteManager) {
+ mBluetoothRouteManager = bluetoothRouteManager;
+ }
+
+ public boolean isAudioDeviceSetForType(@AudioDeviceInfo.AudioDeviceType int audioDeviceType) {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return mAudioDeviceType == audioDeviceType;
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ public int getCurrentLocallyRequestedCommunicationDevice() {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return mAudioDeviceType;
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void setTestCommunicationDevice(@AudioDeviceInfo.AudioDeviceType int audioDeviceType) {
+ mAudioDeviceType = audioDeviceType;
+ }
+
+ public void clearBtCommunicationDevice() {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ 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();
+ }
+ }
+ }
+
+ /*
+ * Sets the communication device for the passed in audio device type, if it's available for
+ * communication use cases. Tries to clear any communication device which was previously
+ * requested for communication before setting the new device.
+ * @param audioDeviceTypes The supported audio device types for the device.
+ * @param btDevice The bluetooth device to connect to (only used for switching between multiple
+ * LE audio devices).
+ * @return {@code true} if the device was set for communication, {@code false} if the device
+ * wasn't set.
+ */
+ public boolean setCommunicationDevice(@AudioDeviceInfo.AudioDeviceType int audioDeviceType,
+ BluetoothDevice btDevice) {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ return processSetCommunicationDevice(audioDeviceType, btDevice);
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ private boolean processSetCommunicationDevice(
+ @AudioDeviceInfo.AudioDeviceType 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",
+ audioDeviceType, isBtDevice, btDevice);
+
+ // Account for switching between multiple LE audio devices.
+ boolean handleLeAudioDeviceSwitch = btDevice != null
+ && !btDevice.getAddress().equals(mBtAudioDevice);
+ if ((audioDeviceType == mAudioDeviceType
+ || isUsbHeadsetType(audioDeviceType, mAudioDeviceType))
+ && !handleLeAudioDeviceSwitch) {
+ Log.i(this, "Communication device is already set for this audio type");
+ return false;
+ }
+
+ AudioDeviceInfo activeDevice = 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());
+ // Ensure that we do not select the same BT LE audio device for communication.
+ if ((audioDeviceType == device.getType()
+ || isUsbHeadsetType(audioDeviceType, device.getType()))
+ && !device.getAddress().equals(mBtAudioDevice)) {
+ activeDevice = device;
+ break;
+ }
+ }
+
+ if (activeDevice == null) {
+ 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)
+ : audioDeviceType);
+ return false;
+ }
+
+ // Force clear previous communication device, if one was set, before setting the new device.
+ if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
+ processClearCommunicationDevice(mAudioDeviceType);
+ }
+
+ // Turn activeDevice ON.
+ boolean result = mAudioManager.setCommunicationDevice(activeDevice);
+ if (!result) {
+ Log.w(this, "Could not set active device");
+ } else {
+ Log.i(this, "Active device set");
+ mAudioDeviceType = activeDevice.getType();
+ if (isBtDevice) {
+ mBtAudioDevice = activeDevice.getAddress();
+ if (audioDeviceType == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+ mBluetoothRouteManager.onAudioOn(mBtAudioDevice);
+ }
+ } else if (Flags.communicationDeviceProtectedByLock()) {
+ // Clear BT device if it's still stored. Handles race condition for when a non-BT
+ // device is set for communication shortly after a BT (LE) device is set for
+ // communication but the selection hasn't been cleared yet.
+ mBtAudioDevice = null;
+ }
+ }
+ return result;
+ }
+ /*
+ * Clears the communication device for the passed in audio device types, given that the device
+ * has previously been set for communication.
+ * @param audioDeviceTypes The supported audio device types for the device.
+ */
+ public void clearCommunicationDevice(@AudioDeviceInfo.AudioDeviceType int audioDeviceType) {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.lock();
+ }
+ try {
+ processClearCommunicationDevice(audioDeviceType);
+ } finally {
+ if (Flags.communicationDeviceProtectedByLock()) {
+ mLock.unlock();
+ }
+ }
+ }
+
+ public void processClearCommunicationDevice(
+ @AudioDeviceInfo.AudioDeviceType 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",
+ audioDeviceType, isBtDevice);
+
+ 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.",
+ audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+ ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ AudioDeviceInfo.TYPE_USB_HEADSET)
+ : audioDeviceType
+ );
+ return;
+ }
+
+ if (mAudioManager == null) {
+ Log.i(this, "clearCommunicationDevice: mAudioManager is null");
+ return;
+ }
+
+ // Clear device and reset locally saved device type.
+ mAudioManager.clearCommunicationDevice();
+ mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
+
+ if (isBtDevice && mBtAudioDevice != null) {
+ // Signal that BT audio was lost for device.
+ mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
+ mBtAudioDevice = null;
+ }
+ }
+
+ private boolean isUsbHeadsetType(@AudioDeviceInfo.AudioDeviceType int audioDeviceType,
+ @AudioDeviceInfo.AudioDeviceType int sourceType) {
+ 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 ff76b9e..d156c0c 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -20,10 +20,11 @@
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;
-import android.telecom.PhoneAccount;
import android.telecom.VideoProfile;
import android.util.SparseArray;
@@ -31,11 +32,15 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.LinkedHashSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
public class CallAudioManager extends CallsManagerListenerBase {
@@ -52,7 +57,7 @@
private final Set<Call> mCalls;
private final SparseArray<LinkedHashSet<Call>> mCallStateToCalls;
- private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ private final CallAudioRouteAdapter mCallAudioRouteAdapter;
private final CallAudioModeStateMachine mCallAudioModeStateMachine;
private final BluetoothStateReceiver mBluetoothStateReceiver;
private final CallsManager mCallsManager;
@@ -60,21 +65,27 @@
private final Ringer mRinger;
private final RingbackPlayer mRingbackPlayer;
private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
+ private final FeatureFlags mFeatureFlags;
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(CallAudioRouteStateMachine callAudioRouteStateMachine,
+ public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter,
CallsManager callsManager,
CallAudioModeStateMachine callAudioModeStateMachine,
InCallTonePlayer.Factory playerFactory,
Ringer ringer,
RingbackPlayer ringbackPlayer,
BluetoothStateReceiver bluetoothStateReceiver,
- DtmfLocalTonePlayer dtmfLocalTonePlayer) {
+ DtmfLocalTonePlayer dtmfLocalTonePlayer,
+ FeatureFlags featureFlags) {
mActiveDialingOrConnectingCalls = new LinkedHashSet<>(1);
mRingingCalls = new LinkedHashSet<>(1);
mHoldingCalls = new LinkedHashSet<>(1);
@@ -92,7 +103,7 @@
put(CallState.AUDIO_PROCESSING, mAudioProcessingCalls);
}};
- mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+ mCallAudioRouteAdapter = callAudioRouteAdapter;
mCallAudioModeStateMachine = callAudioModeStateMachine;
mCallsManager = callsManager;
mPlayerFactory = playerFactory;
@@ -100,10 +111,14 @@
mRingbackPlayer = ringbackPlayer;
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);
- mCallAudioRouteStateMachine.setCallAudioManager(this);
+ mCallAudioRouteAdapter.setCallAudioManager(this);
}
@Override
@@ -116,7 +131,7 @@
// State did not change, so no need to do anything.
return;
}
- Log.d(LOG_TAG, "Call state changed for TC@%s: %s -> %s", call.getId(),
+ Log.i(this, "onCallStateChanged: Call state changed for TC@%s: %s -> %s", call.getId(),
CallState.toString(oldState), CallState.toString(newState));
removeCallFromAllBins(call);
@@ -129,6 +144,12 @@
updateForegroundCall();
if (shouldPlayDisconnectTone(oldState, newState)) {
playToneForDisconnectedCall(call);
+ } else {
+ if (newState == CallState.DISCONNECTED) {
+ // This call is not disconnected, but it won't generate a disconnect tone, so
+ // complete the future to ensure we unbind from BT promptly.
+ completeDisconnectToneFuture(call);
+ }
}
onCallLeavingState(call, oldState);
@@ -220,7 +241,7 @@
// When pulling a video call, automatically enable the speakerphone.
Log.d(LOG_TAG, "Switching to speaker because external video call %s was pulled." +
call.getId());
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.SWITCH_SPEAKER);
}
}
@@ -302,7 +323,7 @@
VideoProfile.isReceptionEnabled(newVideoState);
if (isUpgradeRequest) {
- mPlayerFactory.createPlayer(InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
+ mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
}
}
@@ -311,7 +332,7 @@
// We only play tones for foreground calls.
return;
}
- mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RTT_REQUEST).startTone();
+ mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_RTT_REQUEST).startTone();
}
/**
@@ -324,7 +345,7 @@
*/
@Override
public void onHoldToneRequested(Call call) {
- maybePlayHoldTone();
+ maybePlayHoldTone(call);
}
@Override
@@ -372,7 +393,7 @@
@Override
public void onConnectionServiceChanged(Call call, ConnectionServiceWrapper oldCs,
ConnectionServiceWrapper newCs) {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
}
@@ -390,13 +411,13 @@
Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" +
" to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState),
VideoProfile.videoStateToString(newVideoState));
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.SWITCH_SPEAKER);
}
}
public CallAudioState getCallAudioState() {
- return mCallAudioRouteStateMachine.getCurrentCallAudioState();
+ return mCallAudioRouteAdapter.getCurrentCallAudioState();
}
public Call getPossiblyHeldForegroundCall() {
@@ -417,12 +438,16 @@
Log.v(this, "ignoring toggleMute for emergency call");
return;
}
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.TOGGLE_MUTE);
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void onRingerModeChange() {
+ if (mFeatureFlags.ensureInCarRinging()) {
+ // Stop the current ringtone before attempting to start the new ringtone:
+ stopRinging();
+ }
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.RINGER_MODE_CHANGE, makeArgsForModeStateMachine());
}
@@ -437,7 +462,7 @@
Log.v(this, "ignoring mute for emergency call");
}
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(shouldMute
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(shouldMute
? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
}
@@ -453,23 +478,23 @@
Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
switch (route) {
case CallAudioState.ROUTE_BLUETOOTH:
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH, 0, bluetoothAddress);
return;
case CallAudioState.ROUTE_SPEAKER:
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.USER_SWITCH_SPEAKER);
return;
case CallAudioState.ROUTE_WIRED_HEADSET:
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.USER_SWITCH_HEADSET);
return;
case CallAudioState.ROUTE_EARPIECE:
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
return;
case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE,
CallAudioRouteStateMachine.NO_INCLUDE_BLUETOOTH_IN_BASELINE);
return;
@@ -484,7 +509,7 @@
*/
void switchBaseline() {
Log.i(this, "switchBaseline");
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE,
CallAudioRouteStateMachine.INCLUDE_BLUETOOTH_IN_BASELINE);
}
@@ -528,7 +553,7 @@
synchronized (mCallsManager.getLock()) {
Call localForegroundCall = mForegroundCall;
boolean result = mRinger.startRinging(localForegroundCall,
- mCallAudioRouteStateMachine.isHfpDeviceAvailable());
+ mCallAudioRouteAdapter.isHfpDeviceAvailable());
if (result) {
localForegroundCall.setStartRingTime();
}
@@ -561,8 +586,25 @@
@VisibleForTesting
public void setCallAudioRouteFocusState(int focusState) {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
- CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_FOCUS, focusState, 0);
+ } else {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
+ }
+ }
+
+ public void setCallAudioRouteFocusStateForEndTone() {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS, 1);
+ } else {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ }
}
public void notifyAudioOperationsComplete() {
@@ -571,8 +613,8 @@
}
@VisibleForTesting
- public CallAudioRouteStateMachine getCallAudioRouteStateMachine() {
- return mCallAudioRouteStateMachine;
+ public CallAudioRouteAdapter getCallAudioRouteAdapter() {
+ return mCallAudioRouteAdapter;
}
@VisibleForTesting
@@ -609,9 +651,9 @@
mCallAudioModeStateMachine.dump(pw);
pw.decreaseIndent();
- pw.println("CallAudioRouteStateMachine:");
+ pw.println("mCallAudioRouteAdapter:");
pw.increaseIndent();
- mCallAudioRouteStateMachine.dump(pw);
+ mCallAudioRouteAdapter.dump(pw);
pw.decreaseIndent();
pw.println("BluetoothDeviceManager:");
@@ -623,7 +665,7 @@
}
@VisibleForTesting
- public void setIsTonePlaying(boolean isTonePlaying) {
+ public void setIsTonePlaying(Call call, boolean isTonePlaying) {
Log.i(this, "setIsTonePlaying; isTonePlaying=%b", isTonePlaying);
mIsTonePlaying = isTonePlaying;
mCallAudioModeStateMachine.sendMessageWithArgs(
@@ -632,7 +674,7 @@
makeArgsForModeStateMachine());
if (!isTonePlaying && mIsDisconnectedTonePlaying) {
- mCallsManager.onDisconnectedTonePlaying(false);
+ mCallsManager.onDisconnectedTonePlaying(call, false);
mIsDisconnectedTonePlaying = false;
}
}
@@ -745,10 +787,42 @@
private void onCallEnteringRinging() {
if (mRingingCalls.size() == 1) {
+ 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() {
@@ -761,6 +835,7 @@
private void updateForegroundCall() {
Call oldForegroundCall = mForegroundCall;
+
if (mActiveDialingOrConnectingCalls.size() > 0) {
// Give preference for connecting calls over active/dialing for foreground-ness.
Call possibleConnectingCall = null;
@@ -769,8 +844,27 @@
possibleConnectingCall = call;
}
}
- mForegroundCall = possibleConnectingCall == null ?
- mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
+ if (mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+ // Prefer a connecting call
+ if (possibleConnectingCall != null) {
+ mForegroundCall = possibleConnectingCall;
+ } else {
+ // Next, prefer an active or dialing call which is not in the process of being
+ // disconnected.
+ mForegroundCall = mActiveDialingOrConnectingCalls
+ .stream()
+ .filter(c -> (c.getState() == CallState.ACTIVE
+ || c.getState() == CallState.DIALING)
+ && !c.isLocallyDisconnecting())
+ .findFirst()
+ // If we can't find one, then just fall back to the first one.
+ .orElse(mActiveDialingOrConnectingCalls.iterator().next());
+ }
+ } else {
+ // Legacy (buggy) behavior.
+ mForegroundCall = possibleConnectingCall == null ?
+ mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
+ }
} else if (mRingingCalls.size() > 0) {
mForegroundCall = mRingingCalls.iterator().next();
} else if (mHoldingCalls.size() > 0) {
@@ -778,12 +872,27 @@
} else {
mForegroundCall = null;
}
-
+ Log.i(this, "updateForegroundCall; oldFg=%s, newFg=%s, aDC=%s, ring=%s, hold=%s",
+ (oldForegroundCall == null ? "none" : oldForegroundCall.getId()),
+ (mForegroundCall == null ? "none" : mForegroundCall.getId()),
+ mActiveDialingOrConnectingCalls.stream().map(c -> c.getId()).collect(
+ Collectors.joining(",")),
+ mRingingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(",")),
+ mHoldingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(","))
+ );
if (mForegroundCall != oldForegroundCall) {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+ if (mForegroundCall != null
+ && mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+ // Ensure the voip audio mode for the new foreground call is taken into account.
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
+ makeArgsForModeStateMachine());
+ }
mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
- maybePlayHoldTone();
+ maybePlayHoldTone(oldForegroundCall);
}
}
@@ -845,12 +954,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;
}
@@ -886,11 +997,13 @@
Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
- boolean didToneStart = mPlayerFactory.createPlayer(toneToPlay).startTone();
+ boolean didToneStart = mPlayerFactory.createPlayer(call, toneToPlay).startTone();
if (didToneStart) {
- mCallsManager.onDisconnectedTonePlaying(true);
+ mCallsManager.onDisconnectedTonePlaying(call, true);
mIsDisconnectedTonePlaying = true;
}
+ } else {
+ completeDisconnectToneFuture(call);
}
}
}
@@ -908,10 +1021,11 @@
/**
* Determines if a hold tone should be played and then starts or stops it accordingly.
*/
- private void maybePlayHoldTone() {
+ private void maybePlayHoldTone(Call call) {
if (shouldPlayHoldTone()) {
if (mHoldTonePlayer == null) {
- mHoldTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+ mHoldTonePlayer = mPlayerFactory.createPlayer(call,
+ InCallTonePlayer.TONE_CALL_WAITING);
mHoldTonePlayer.startTone();
}
} else {
@@ -977,6 +1091,21 @@
oldState == CallState.ON_HOLD;
}
+ private void completeDisconnectToneFuture(Call call) {
+ CompletableFuture<Void> disconnectedToneFuture = mCallsManager.getInCallController()
+ .getDisconnectedToneBtFutures().get(call.getId());
+ if (disconnectedToneFuture != null) {
+ Log.i(this,
+ "completeDisconnectToneFuture: completing deferred disconnect tone future for"
+ + " call %s",
+ call.getId());
+ disconnectedToneFuture.complete(null);
+ }
+ // Make sure we schedule the unbinding of the BT ICS once the disconnected tone future has
+ // been completed.
+ mCallsManager.getInCallController().maybeScheduleBtUnbind(call);
+ }
+
@VisibleForTesting
public Set<Call> getTrackedCalls() {
return mCalls;
@@ -986,4 +1115,9 @@
public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
return mCallStateToCalls;
}
+
+ @VisibleForTesting
+ public CompletableFuture<Boolean> getCallRingingFuture() {
+ return mCallRingingFuture;
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 9ad9094..e149bdd 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -16,10 +16,10 @@
package com.android.server.telecom;
+import android.media.AudioAttributes;
import android.media.AudioManager;
import android.os.Looper;
import android.os.Message;
-import android.os.Trace;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
@@ -29,6 +29,7 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.server.telecom.flags.FeatureFlags;
public class CallAudioModeStateMachine extends StateMachine {
/**
@@ -38,8 +39,10 @@
private LocalLog mLocalLog = new LocalLog(20);
public static class Factory {
public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
- AudioManager am) {
- return new CallAudioModeStateMachine(systemStateHelper, am);
+ AudioManager am, FeatureFlags featureFlags,
+ CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) {
+ return new CallAudioModeStateMachine(systemStateHelper, am,
+ featureFlags, callAudioCommunicationDeviceTracker);
}
}
@@ -256,8 +259,25 @@
Log.i(LOG_TAG, "Audio focus entering UNFOCUSED state");
mLocalLog.log("Enter UNFOCUSED");
if (mIsInitialized) {
- mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS);
- mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ // Clear any communication device that was requested previously.
+ // Todo: Remove once clearCommunicationDeviceAfterAudioOpsComplete is
+ // completely rolled out.
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()
+ && !mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(mCommunicationDeviceTracker
+ .getCurrentLocallyRequestedCommunicationDevice());
+ }
+ if (mFeatureFlags.setAudioModeBeforeAbandonFocus()) {
+ Log.i(this, "enter: AudioManager#setMode(MODE_NORMAL)");
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ mCallAudioManager.setCallAudioRouteFocusState(
+ CallAudioRouteStateMachine.NO_FOCUS);
+ } else {
+ mCallAudioManager.setCallAudioRouteFocusState(
+ CallAudioRouteStateMachine.NO_FOCUS);
+ Log.i(this, "enter: AudioManager#setMode(MODE_NORMAL)");
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ }
mLocalLog.log("Mode MODE_NORMAL");
mMostRecentMode = AudioManager.MODE_NORMAL;
// Don't release focus here -- wait until we get a signal that any other audio
@@ -309,8 +329,15 @@
+ args.toString());
return HANDLED;
case AUDIO_OPERATIONS_COMPLETE:
- Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED");
+ Log.i(this, "AudioOperationsComplete: "
+ + "AudioManager#abandonAudioFocusRequest(); now unfocused");
mAudioManager.abandonAudioFocusForCall();
+ // Clear requested communication device after the call ends.
+ if (mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ mCommunicationDeviceTracker
+ .getCurrentLocallyRequestedCommunicationDevice());
+ }
return HANDLED;
default:
// The forced focus switch commands are handled by BaseState.
@@ -326,6 +353,7 @@
mLocalLog.log("Enter AUDIO_PROCESSING");
if (mIsInitialized) {
mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS);
+ Log.i(this, "enter: AudioManager#setMode(MODE_AUDIO_PROCESSING)");
mAudioManager.setMode(NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING);
mLocalLog.log("Mode MODE_CALL_SCREENING");
mMostRecentMode = NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING;
@@ -380,7 +408,8 @@
transitionTo(mStreamingFocusState);
return HANDLED;
case AUDIO_OPERATIONS_COMPLETE:
- Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING");
+ Log.i(LOG_TAG, "AudioManager#abandonAudioFocusRequest: now "
+ + "AUDIO_PROCESSING");
mAudioManager.abandonAudioFocusForCall();
return HANDLED;
default:
@@ -396,33 +425,31 @@
private boolean mHasFocus = false;
private void tryStartRinging() {
- Trace.traceBegin(Trace.TRACE_TAG_AUDIO, "CallAudioMode.tryStartRinging");
- try {
- if (mHasFocus && mCallAudioManager.isRingtonePlaying()) {
- Log.i(LOG_TAG,
- "RingingFocusState#tryStartRinging -- audio focus previously"
- + " acquired and ringtone already playing -- skipping.");
- return;
- }
+ if (mHasFocus && mCallAudioManager.isRingtonePlaying()) {
+ Log.i(LOG_TAG,
+ "RingingFocusState#tryStartRinging -- audio focus previously"
+ + " acquired and ringtone already playing -- skipping.");
+ return;
+ }
- if (mCallAudioManager.startRinging()) {
- mAudioManager.requestAudioFocusForCall(
+ if (mCallAudioManager.startRinging()) {
+ Log.i(this, "tryStartRinging: AudioManager#requestAudioFocus(RING)");
+ mAudioManager.requestAudioFocusForCall(
AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
- // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode --
- // this trips up the audio system.
- if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) {
- mAudioManager.setMode(AudioManager.MODE_RINGTONE);
- mLocalLog.log("Mode MODE_RINGTONE");
- }
- mCallAudioManager.setCallAudioRouteFocusState(
- CallAudioRouteStateMachine.RINGING_FOCUS);
- mHasFocus = true;
- } else {
- Log.i(
- LOG_TAG, "RINGING state, try start ringing but not acquiring audio focus");
+
+ // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode --
+ // this trips up the audio system.
+ if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) {
+ Log.i(this, "enter: AudioManager#setMode(MODE_RINGTONE)");
+ mAudioManager.setMode(AudioManager.MODE_RINGTONE);
+ mLocalLog.log("Mode MODE_RINGTONE");
}
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_AUDIO);
+ mCallAudioManager.setCallAudioRouteFocusState(
+ CallAudioRouteStateMachine.RINGING_FOCUS);
+ mHasFocus = true;
+ } else {
+ Log.i(
+ LOG_TAG, "RINGING state, try start ringing but not acquiring audio focus");
}
}
@@ -504,8 +531,10 @@
public void enter() {
Log.i(LOG_TAG, "Audio focus entering SIM CALL state");
mLocalLog.log("Enter SIM_CALL");
+ Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ Log.i(this, "enter: AudioManager#setMode(MODE_IN_CALL)");
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
mLocalLog.log("Mode MODE_IN_CALL");
mMostRecentMode = AudioManager.MODE_IN_CALL;
@@ -587,8 +616,10 @@
public void enter() {
Log.i(LOG_TAG, "Audio focus entering VOIP CALL state");
mLocalLog.log("Enter VOIP_CALL");
+ Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ Log.i(this, "enter: AudioManager#setMode(MODE_IN_COMMUNICATION)");
mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
mLocalLog.log("Mode MODE_IN_COMMUNICATION");
mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
@@ -667,15 +698,16 @@
Log.i(LOG_TAG, "Audio focus entering streaming state");
mLocalLog.log("Enter Streaming");
mLocalLog.log("Mode MODE_COMMUNICATION_REDIRECT");
+ Log.i(this, "enter: AudioManager#setMode(MODE_COMMUNICATION_REDIRECT");
mAudioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT);
mMostRecentMode = AudioManager.MODE_NORMAL;
mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
- mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo(
+ mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo(
CallAudioRouteStateMachine.STREAMING_FORCE_ENABLED);
}
private void preExit() {
- mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo(
+ mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo(
CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
}
@@ -742,11 +774,13 @@
public void enter() {
Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state");
mLocalLog.log("Enter TONE/HOLDING");
+ Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ Log.i(this, "enter: AudioManager#setMode(%d)", mMostRecentMode);
mAudioManager.setMode(mMostRecentMode);
mLocalLog.log("Mode " + mMostRecentMode);
- mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ mCallAudioManager.setCallAudioRouteFocusStateForEndTone();
}
@Override
@@ -815,16 +849,21 @@
private final AudioManager mAudioManager;
private final SystemStateHelper mSystemStateHelper;
private CallAudioManager mCallAudioManager;
+ private FeatureFlags mFeatureFlags;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
private int mMostRecentMode;
private boolean mIsInitialized = false;
public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
- AudioManager audioManager) {
+ AudioManager audioManager, FeatureFlags featureFlags,
+ CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) {
super(CallAudioModeStateMachine.class.getSimpleName());
mAudioManager = audioManager;
mSystemStateHelper = systemStateHelper;
mMostRecentMode = AudioManager.MODE_NORMAL;
+ mFeatureFlags = featureFlags;
+ mCommunicationDeviceTracker = callAudioCommunicationDeviceTracker;
createStates();
}
@@ -833,11 +872,14 @@
* Used for testing
*/
public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
- AudioManager audioManager, Looper looper) {
+ AudioManager audioManager, Looper looper, FeatureFlags featureFlags,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
super(CallAudioModeStateMachine.class.getSimpleName(), looper);
mAudioManager = audioManager;
mSystemStateHelper = systemStateHelper;
mMostRecentMode = AudioManager.MODE_NORMAL;
+ mFeatureFlags = featureFlags;
+ mCommunicationDeviceTracker = communicationDeviceTracker;
createStates();
}
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
new file mode 100644
index 0000000..b23851d
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -0,0 +1,140 @@
+package com.android.server.telecom;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.Handler;
+import android.telecom.CallAudioState;
+import android.util.SparseArray;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+public interface CallAudioRouteAdapter {
+ /** Valid values for msg.what */
+ int CONNECT_WIRED_HEADSET = 1;
+ int DISCONNECT_WIRED_HEADSET = 2;
+ int CONNECT_DOCK = 5;
+ int DISCONNECT_DOCK = 6;
+ int BLUETOOTH_DEVICE_LIST_CHANGED = 7;
+ int BT_ACTIVE_DEVICE_PRESENT = 8;
+ int BT_ACTIVE_DEVICE_GONE = 9;
+ int BT_DEVICE_ADDED = 10;
+ int BT_DEVICE_REMOVED = 11;
+
+ int SWITCH_EARPIECE = 1001;
+ int SWITCH_BLUETOOTH = 1002;
+ int SWITCH_HEADSET = 1003;
+ int SWITCH_SPEAKER = 1004;
+ // Wired headset, earpiece, or speakerphone, in that order of precedence.
+ int SWITCH_BASELINE_ROUTE = 1005;
+
+ // Messages denoting that the speakerphone was turned on/off. Used to update state when we
+ // weren't the ones who turned it on/off
+ int SPEAKER_ON = 1006;
+ int SPEAKER_OFF = 1007;
+
+ // Messages denoting that the streaming route switch request was sent.
+ int STREAMING_FORCE_ENABLED = 1008;
+ int STREAMING_FORCE_DISABLED = 1009;
+
+ int USER_SWITCH_EARPIECE = 1101;
+ int USER_SWITCH_BLUETOOTH = 1102;
+ int USER_SWITCH_HEADSET = 1103;
+ int USER_SWITCH_SPEAKER = 1104;
+ int USER_SWITCH_BASELINE_ROUTE = 1105;
+
+ int UPDATE_SYSTEM_AUDIO_ROUTE = 1201;
+
+ // These three messages indicate state changes that come from BluetoothRouteManager.
+ // They may be triggered by the BT stack doing something on its own or they may be sent after
+ // we request that the BT stack do something. Any logic for these messages should take into
+ // account the possibility that the event indicated has already been processed (i.e. handling
+ // should be idempotent).
+ int BT_AUDIO_DISCONNECTED = 1301;
+ int BT_AUDIO_CONNECTED = 1302;
+ int BT_AUDIO_PENDING = 1303;
+
+ int MUTE_ON = 3001;
+ int MUTE_OFF = 3002;
+ int TOGGLE_MUTE = 3003;
+ int MUTE_EXTERNALLY_CHANGED = 3004;
+
+ int SWITCH_FOCUS = 4001;
+
+ // Used in testing to execute verifications. Not compatible with subsessions.
+ int RUN_RUNNABLE = 9001;
+
+ // Used for PendingAudioRoute to notify audio switch success
+ int EXIT_PENDING_ROUTE = 10001;
+ // Used for PendingAudioRoute to notify audio switch timeout
+ int PENDING_ROUTE_TIMEOUT = 10002;
+ // Used for PendingAudioRoute to notify audio switch failed
+ int PENDING_ROUTE_FAILED = 10003;
+
+ /** Valid values for mAudioFocusType */
+ int NO_FOCUS = 1;
+ int ACTIVE_FOCUS = 2;
+ int RINGING_FOCUS = 3;
+
+ /** Valid arg for BLUETOOTH_DEVICE_LIST_CHANGED */
+ int DEVICE_CONNECTED = 1;
+ int DEVICE_DISCONNECTED = 2;
+
+ SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
+ put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
+ put(DISCONNECT_WIRED_HEADSET, "DISCONNECT_WIRED_HEADSET");
+ put(CONNECT_DOCK, "CONNECT_DOCK");
+ put(DISCONNECT_DOCK, "DISCONNECT_DOCK");
+ put(BLUETOOTH_DEVICE_LIST_CHANGED, "BLUETOOTH_DEVICE_LIST_CHANGED");
+ put(BT_ACTIVE_DEVICE_PRESENT, "BT_ACTIVE_DEVICE_PRESENT");
+ put(BT_ACTIVE_DEVICE_GONE, "BT_ACTIVE_DEVICE_GONE");
+ put(BT_DEVICE_ADDED, "BT_DEVICE_ADDED");
+ put(BT_DEVICE_REMOVED, "BT_DEVICE_REMOVED");
+
+ put(SWITCH_EARPIECE, "SWITCH_EARPIECE");
+ put(SWITCH_BLUETOOTH, "SWITCH_BLUETOOTH");
+ put(SWITCH_HEADSET, "SWITCH_HEADSET");
+ put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
+ put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
+ put(SPEAKER_ON, "SPEAKER_ON");
+ put(SPEAKER_OFF, "SPEAKER_OFF");
+
+ put(STREAMING_FORCE_ENABLED, "STREAMING_FORCE_ENABLED");
+ put(STREAMING_FORCE_DISABLED, "STREAMING_FORCE_DISABLED");
+
+ put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
+ put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
+ put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
+ put(USER_SWITCH_SPEAKER, "USER_SWITCH_SPEAKER");
+ put(USER_SWITCH_BASELINE_ROUTE, "USER_SWITCH_BASELINE_ROUTE");
+
+ put(UPDATE_SYSTEM_AUDIO_ROUTE, "UPDATE_SYSTEM_AUDIO_ROUTE");
+
+ put(BT_AUDIO_DISCONNECTED, "BT_AUDIO_DISCONNECTED");
+ put(BT_AUDIO_CONNECTED, "BT_AUDIO_CONNECTED");
+ put(BT_AUDIO_PENDING, "BT_AUDIO_PENDING");
+
+ put(MUTE_ON, "MUTE_ON");
+ put(MUTE_OFF, "MUTE_OFF");
+ put(TOGGLE_MUTE, "TOGGLE_MUTE");
+ put(MUTE_EXTERNALLY_CHANGED, "MUTE_EXTERNALLY_CHANGED");
+
+ put(SWITCH_FOCUS, "SWITCH_FOCUS");
+
+ put(RUN_RUNNABLE, "RUN_RUNNABLE");
+
+ put(EXIT_PENDING_ROUTE, "EXIT_PENDING_ROUTE");
+ }};
+
+ void initialize();
+ void sendMessageWithSessionInfo(int message);
+ void sendMessageWithSessionInfo(int message, int arg);
+ void sendMessageWithSessionInfo(int message, int arg, String data);
+ void sendMessageWithSessionInfo(int message, int arg, int data);
+ void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice);
+ void sendMessage(int message, Runnable r);
+ void setCallAudioManager(CallAudioManager callAudioManager);
+ 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
new file mode 100644
index 0000000..04f1934
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -0,0 +1,1619 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
+import static com.android.server.telecom.AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE;
+import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
+
+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.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.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.RemoteException;
+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;
+
+import com.android.internal.annotations.VisibleForTesting;
+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 com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class CallAudioRouteController implements CallAudioRouteAdapter {
+ private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null);
+ private static final Map<Integer, Integer> ROUTE_MAP;
+ static {
+ ROUTE_MAP = new ArrayMap<>();
+ ROUTE_MAP.put(TYPE_INVALID, 0);
+ ROUTE_MAP.put(AudioRoute.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
+ ROUTE_MAP.put(AudioRoute.TYPE_WIRED, CallAudioState.ROUTE_WIRED_HEADSET);
+ ROUTE_MAP.put(AudioRoute.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
+ ROUTE_MAP.put(AudioRoute.TYPE_DOCK, CallAudioState.ROUTE_SPEAKER);
+ ROUTE_MAP.put(AudioRoute.TYPE_BUS, CallAudioState.ROUTE_SPEAKER);
+ ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH);
+ ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH);
+ ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH);
+ ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
+ }
+
+ /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
+ public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
+
+ private final CallsManager mCallsManager;
+ private final Context mContext;
+ private AudioManager mAudioManager;
+ private CallAudioManager mCallAudioManager;
+ private final BluetoothRouteManager mBluetoothRouteManager;
+ private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+ private final Handler mHandler;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+ private Set<AudioRoute> mAvailableRoutes;
+ private Set<AudioRoute> mCallSupportedRoutes;
+ private AudioRoute mCurrentRoute;
+ private AudioRoute mEarpieceWiredRoute;
+ private AudioRoute mSpeakerDockRoute;
+ private AudioRoute mStreamingRoute;
+ private Set<AudioRoute> mStreamingRoutes;
+ private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
+ private Pair<Integer, String> mActiveBluetoothDevice;
+ private Map<Integer, String> mActiveDeviceCache;
+ private String mBluetoothAddressForRinging;
+ private Map<Integer, AudioRoute> mTypeRoutes;
+ private PendingAudioRoute mPendingAudioRoute;
+ private AudioRoute.Factory mAudioRouteFactory;
+ private StatusBarNotifier mStatusBarNotifier;
+ private AudioManager.OnCommunicationDeviceChangedListener mCommunicationDeviceListener;
+ private ExecutorService mCommunicationDeviceChangedExecutor;
+ private FeatureFlags mFeatureFlags;
+ private int mFocusType;
+ private int mCallSupportedRouteMask = -1;
+ private boolean mIsScoAudioConnected;
+ private boolean mAvailableRoutesUpdated;
+ 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) {
+ Log.startSession("CARC.mSPCR");
+ try {
+ if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) {
+ if (mAudioManager != null) {
+ AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
+ if ((info != null) &&
+ (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
+ if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
+ } else {
+ sendMessageWithSessionInfo(SPEAKER_OFF);
+ }
+ }
+ } else {
+ Log.w(this, "Received non-speakerphone-change intent");
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+ private final BroadcastReceiver mMuteChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.startSession("CARC.mCR");
+ try {
+ if (AudioManager.ACTION_MICROPHONE_MUTE_CHANGED.equals(intent.getAction())) {
+ if (mCallsManager.isInEmergencyCall()) {
+ Log.i(this, "Mute was externally changed when there's an emergency call. "
+ + "Forcing mute back off.");
+ sendMessageWithSessionInfo(MUTE_OFF);
+ } else {
+ sendMessageWithSessionInfo(MUTE_EXTERNALLY_CHANGED);
+ }
+ } else if (AudioManager.STREAM_MUTE_CHANGED_ACTION.equals(intent.getAction())) {
+ int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+ boolean isStreamMuted = intent.getBooleanExtra(
+ AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+
+ if (streamType == AudioManager.STREAM_RING && !isStreamMuted
+ && mCallAudioManager != null) {
+ Log.i(this, "Ring stream was un-muted.");
+ mCallAudioManager.onRingerModeChange();
+ }
+ } else {
+ Log.w(this, "Received non-mute-change intent");
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+ private CallAudioState mCallAudioState;
+ private boolean mIsMute;
+ private boolean mIsPending;
+ private boolean mIsActive;
+ private final TelecomMetricsController mMetricsController;
+
+ public CallAudioRouteController(
+ Context context, CallsManager callsManager,
+ CallAudioManager.AudioServiceFactory audioServiceFactory,
+ AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
+ BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
+ FeatureFlags featureFlags, TelecomMetricsController metricsController) {
+ mContext = context;
+ mCallsManager = callsManager;
+ mAudioManager = context.getSystemService(AudioManager.class);
+ mAudioServiceFactory = audioServiceFactory;
+ mAudioRouteFactory = audioRouteFactory;
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mIsMute = false;
+ mBluetoothRouteManager = bluetoothRouteManager;
+ mStatusBarNotifier = statusBarNotifier;
+ mFeatureFlags = featureFlags;
+ mMetricsController = metricsController;
+ mFocusType = NO_FOCUS;
+ mIsScoAudioConnected = false;
+ mTelecomLock = callsManager.getLock();
+ HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
+ handlerThread.start();
+
+ // Register broadcast receivers
+ if (!mFeatureFlags.newAudioPathSpeakerBroadcastAndUnfocusedRouting()) {
+ IntentFilter speakerChangedFilter = new IntentFilter(
+ AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
+ speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
+ }
+
+ IntentFilter micMuteChangedFilter = new IntentFilter(
+ AudioManager.ACTION_MICROPHONE_MUTE_CHANGED);
+ micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter);
+
+ IntentFilter muteChangedFilter = new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+ muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mMuteChangeReceiver, muteChangedFilter);
+
+ // Register AudioManager#onCommunicationDeviceChangedListener listener to receive updates
+ // to communication device (via AudioManager#setCommunicationDevice). This is a replacement
+ // to using broadcasts in the hopes of improving performance.
+ mCommunicationDeviceChangedExecutor = Executors.newSingleThreadExecutor();
+ mCommunicationDeviceListener = new AudioManager.OnCommunicationDeviceChangedListener() {
+ @Override
+ public void onCommunicationDeviceChanged(AudioDeviceInfo device) {
+ @AudioRoute.AudioRouteType int audioType = device != null
+ ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(device.getType())
+ : TYPE_INVALID;
+ Log.i(this, "onCommunicationDeviceChanged: %d", audioType);
+ if (device != null && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ if (mCurrentRoute.getType() != TYPE_SPEAKER) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
+ } else {
+ sendMessageWithSessionInfo(SPEAKER_OFF);
+ }
+ }
+ };
+
+ // Create handler
+ mHandler = new Handler(handlerThread.getLooper()) {
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ synchronized (this) {
+ preHandleMessage(msg);
+ String address;
+ BluetoothDevice bluetoothDevice;
+ int focus;
+ int handleEndTone;
+ @AudioRoute.AudioRouteType int type;
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ handleWiredHeadsetConnected();
+ break;
+ case DISCONNECT_WIRED_HEADSET:
+ handleWiredHeadsetDisconnected();
+ break;
+ case CONNECT_DOCK:
+ handleDockConnected();
+ break;
+ case DISCONNECT_DOCK:
+ handleDockDisconnected();
+ break;
+ case BLUETOOTH_DEVICE_LIST_CHANGED:
+ break;
+ case BT_ACTIVE_DEVICE_PRESENT:
+ type = msg.arg1;
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleBtActiveDevicePresent(type, address);
+ break;
+ case BT_ACTIVE_DEVICE_GONE:
+ type = msg.arg1;
+ handleBtActiveDeviceGone(type);
+ break;
+ case BT_DEVICE_ADDED:
+ type = msg.arg1;
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtConnected(type, bluetoothDevice);
+ break;
+ case BT_DEVICE_REMOVED:
+ type = msg.arg1;
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtDisconnected(type, bluetoothDevice);
+ break;
+ case SWITCH_EARPIECE:
+ case USER_SWITCH_EARPIECE:
+ handleSwitchEarpiece();
+ break;
+ case SWITCH_BLUETOOTH:
+ case USER_SWITCH_BLUETOOTH:
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleSwitchBluetooth(address);
+ break;
+ case SWITCH_HEADSET:
+ case USER_SWITCH_HEADSET:
+ handleSwitchHeadset();
+ break;
+ case SWITCH_SPEAKER:
+ case USER_SWITCH_SPEAKER:
+ handleSwitchSpeaker();
+ break;
+ case SWITCH_BASELINE_ROUTE:
+ address = (String) ((SomeArgs) msg.obj).arg2;
+ handleSwitchBaselineRoute(false,
+ msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, address);
+ break;
+ case USER_SWITCH_BASELINE_ROUTE:
+ handleSwitchBaselineRoute(true,
+ msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, null);
+ break;
+ case SPEAKER_ON:
+ handleSpeakerOn();
+ break;
+ case SPEAKER_OFF:
+ handleSpeakerOff();
+ break;
+ case STREAMING_FORCE_ENABLED:
+ handleStreamingEnabled();
+ break;
+ case STREAMING_FORCE_DISABLED:
+ handleStreamingDisabled();
+ break;
+ case BT_AUDIO_CONNECTED:
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtAudioActive(bluetoothDevice);
+ break;
+ case BT_AUDIO_DISCONNECTED:
+ bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+ handleBtAudioInactive(bluetoothDevice);
+ break;
+ case MUTE_ON:
+ handleMuteChanged(true);
+ break;
+ case MUTE_OFF:
+ handleMuteChanged(false);
+ break;
+ case MUTE_EXTERNALLY_CHANGED:
+ handleMuteChanged(mAudioManager.isMicrophoneMute());
+ break;
+ case SWITCH_FOCUS:
+ focus = msg.arg1;
+ handleEndTone = (int) ((SomeArgs) msg.obj).arg2;
+ handleSwitchFocus(focus, handleEndTone);
+ break;
+ case EXIT_PENDING_ROUTE:
+ handleExitPendingRoute();
+ break;
+ case UPDATE_SYSTEM_AUDIO_ROUTE:
+ // Based on the available routes for foreground call, adjust routing.
+ updateRouteForForeground();
+ // Force update to notify all ICS/CS.
+ updateCallAudioState(new CallAudioState(mIsMute,
+ mCallAudioState.getRoute(),
+ mCallAudioState.getSupportedRouteMask(),
+ mCallAudioState.getActiveBluetoothDevice(),
+ mCallAudioState.getSupportedBluetoothDevices()));
+ default:
+ break;
+ }
+ postHandleMessage(msg);
+ }
+ }
+ };
+ }
+ @Override
+ public void initialize() {
+ mAvailableRoutes = new HashSet<>();
+ mCallSupportedRoutes = new HashSet<>();
+ mBluetoothRoutes = Collections.synchronizedMap(new LinkedHashMap<>());
+ mActiveDeviceCache = new HashMap<>();
+ mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
+ mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null);
+ mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null);
+ mActiveBluetoothDevice = null;
+ mTypeRoutes = new ArrayMap<>();
+ mStreamingRoutes = new HashSet<>();
+ mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager,
+ mFeatureFlags);
+ mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
+ mStreamingRoutes.add(mStreamingRoute);
+
+ int supportMask = calculateSupportedRouteMaskInit();
+ if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
+ int audioRouteType = AudioRoute.TYPE_SPEAKER;
+ // Create speaker routes
+ mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
+ mAudioManager);
+ if (mSpeakerDockRoute == null){
+ Log.i(this, "Can't find available audio device info for route TYPE_SPEAKER, trying"
+ + " for TYPE_BUS");
+ mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_BUS, null,
+ mAudioManager);
+ audioRouteType = AudioRoute.TYPE_BUS;
+ }
+ if (mSpeakerDockRoute != null) {
+ mTypeRoutes.put(audioRouteType, mSpeakerDockRoute);
+ updateAvailableRoutes(mSpeakerDockRoute, true);
+ } else {
+ Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER "
+ + "or TYPE_BUS.");
+ }
+ }
+
+ if ((supportMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) {
+ // Create wired headset routes
+ mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+ mAudioManager);
+ if (mEarpieceWiredRoute == null) {
+ Log.w(this, "Can't find available audio device info for route TYPE_WIRED_HEADSET");
+ } else {
+ mTypeRoutes.put(AudioRoute.TYPE_WIRED, mEarpieceWiredRoute);
+ updateAvailableRoutes(mEarpieceWiredRoute, true);
+ }
+ } else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) {
+ // Create earpiece routes
+ mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
+ mAudioManager);
+ if (mEarpieceWiredRoute == null) {
+ Log.w(this, "Can't find available audio device info for route TYPE_EARPIECE");
+ } else {
+ mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
+ updateAvailableRoutes(mEarpieceWiredRoute, true);
+ }
+ }
+
+ // set current route
+ if (mEarpieceWiredRoute != null) {
+ mCurrentRoute = mEarpieceWiredRoute;
+ } else if (mSpeakerDockRoute != null) {
+ mCurrentRoute = mSpeakerDockRoute;
+ } else {
+ mCurrentRoute = DUMMY_ROUTE;
+ }
+ mIsActive = false;
+ mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
+ supportMask, null, new HashSet<>());
+ if (mFeatureFlags.newAudioPathSpeakerBroadcastAndUnfocusedRouting()) {
+ mAudioManager.addOnCommunicationDeviceChangedListener(
+ mCommunicationDeviceChangedExecutor,
+ mCommunicationDeviceListener);
+ }
+ }
+
+ @Override
+ public void sendMessageWithSessionInfo(int message) {
+ sendMessageWithSessionInfo(message, 0, (String) null);
+ }
+
+ @Override
+ public void sendMessageWithSessionInfo(int message, int arg) {
+ sendMessageWithSessionInfo(message, arg, (String) null);
+ }
+
+ @Override
+ public void sendMessageWithSessionInfo(int message, int arg, String data) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = Log.createSubsession();
+ args.arg2 = data;
+ sendMessage(message, arg, 0, args);
+ }
+
+ @Override
+ public void sendMessageWithSessionInfo(int message, int arg, int data) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = Log.createSubsession();
+ args.arg2 = data;
+ sendMessage(message, arg, 0, args);
+ }
+
+ @Override
+ public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = Log.createSubsession();
+ args.arg2 = bluetoothDevice;
+ sendMessage(message, arg, 0, args);
+ }
+
+ @Override
+ public void sendMessage(int message, Runnable r) {
+ r.run();
+ }
+
+ private void sendMessage(int what, int arg1, int arg2, Object obj) {
+ mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2, obj));
+ }
+
+ @Override
+ public void setCallAudioManager(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
+ }
+
+ @Override
+ public CallAudioState getCurrentCallAudioState() {
+ return mCallAudioState;
+ }
+
+ @Override
+ public boolean isHfpDeviceAvailable() {
+ return !mBluetoothRoutes.isEmpty();
+ }
+
+ @Override
+ public Handler getAdapterHandler() {
+ return mHandler;
+ }
+
+ @Override
+ public PendingAudioRoute getPendingAudioRoute() {
+ return mPendingAudioRoute;
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter pw) {
+ }
+
+ private void preHandleMessage(Message msg) {
+ if (msg.obj instanceof SomeArgs) {
+ Session session = (Session) ((SomeArgs) msg.obj).arg1;
+ String messageCodeName = MESSAGE_CODE_TO_NAME.get(msg.what, "unknown");
+ Log.continueSession(session, "CARC.pM_" + messageCodeName);
+ Log.i(this, "Message received: %s=%d, arg1=%d", messageCodeName, msg.what, msg.arg1);
+ }
+ }
+
+ private void postHandleMessage(Message msg) {
+ Log.endSession();
+ if (msg.obj instanceof SomeArgs) {
+ ((SomeArgs) msg.obj).recycle();
+ }
+ }
+
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ public boolean isPending() {
+ return mIsPending;
+ }
+
+ private void routeTo(boolean active, AudioRoute destRoute) {
+ if (destRoute == null || (!destRoute.equals(mStreamingRoute)
+ && !getCallSupportedRoutes().contains(destRoute))) {
+ Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+ ErrorStats.ERROR_AUDIO_ROUTE_UNAVAILABLE);
+ }
+ return;
+ }
+ if (mIsPending) {
+ if (destRoute.equals(mPendingAudioRoute.getDestRoute()) && (mIsActive == active)) {
+ return;
+ }
+ 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 (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && 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
+ mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
+ } else {
+ if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
+ return;
+ }
+ Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
+ mIsActive, destRoute, active);
+ // route to pending route
+ if (getCallSupportedRoutes().contains(mCurrentRoute)) {
+ mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
+ } else {
+ // Avoid waiting for pending messages for an unavailable route
+ mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
+ }
+ mIsPending = true;
+ }
+ mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+ mIsScoAudioConnected);
+ mIsActive = active;
+ mPendingAudioRoute.evaluatePendingState();
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getAudioRouteStats().onRouteEnter(mPendingAudioRoute);
+ }
+ }
+
+ private void handleWiredHeadsetConnected() {
+ AudioRoute wiredHeadsetRoute = null;
+ try {
+ wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+ mAudioManager);
+ } catch (IllegalArgumentException e) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+ ErrorStats.ERROR_EXTERNAL_EXCEPTION);
+ }
+ Log.e(this, e, "Can't find available audio device info for route type:"
+ + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+ }
+
+ if (wiredHeadsetRoute != null) {
+ updateAvailableRoutes(wiredHeadsetRoute, true);
+ updateAvailableRoutes(mEarpieceWiredRoute, false);
+ mTypeRoutes.put(AudioRoute.TYPE_WIRED, wiredHeadsetRoute);
+ mEarpieceWiredRoute = wiredHeadsetRoute;
+ routeTo(mIsActive, wiredHeadsetRoute);
+ onAvailableRoutesChanged();
+ }
+ }
+
+ public void handleWiredHeadsetDisconnected() {
+ // Update audio route states
+ AudioRoute wiredHeadsetRoute = mTypeRoutes.remove(AudioRoute.TYPE_WIRED);
+ if (wiredHeadsetRoute != null) {
+ updateAvailableRoutes(wiredHeadsetRoute, false);
+ mEarpieceWiredRoute = null;
+ }
+ AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+ if (earpieceRoute != null) {
+ updateAvailableRoutes(earpieceRoute, true);
+ mEarpieceWiredRoute = earpieceRoute;
+ }
+ onAvailableRoutesChanged();
+
+ // Route to expected state
+ if (mCurrentRoute.equals(wiredHeadsetRoute)) {
+ routeTo(mIsActive, getBaseRoute(true, null));
+ }
+ }
+
+ private void handleDockConnected() {
+ AudioRoute dockRoute = null;
+ try {
+ dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager);
+ } catch (IllegalArgumentException e) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+ ErrorStats.ERROR_EXTERNAL_EXCEPTION);
+ }
+ Log.e(this, e, "Can't find available audio device info for route type:"
+ + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+ }
+
+ if (dockRoute != null) {
+ updateAvailableRoutes(dockRoute, true);
+ updateAvailableRoutes(mSpeakerDockRoute, false);
+ mTypeRoutes.put(AudioRoute.TYPE_DOCK, dockRoute);
+ mSpeakerDockRoute = dockRoute;
+ routeTo(mIsActive, dockRoute);
+ onAvailableRoutesChanged();
+ }
+ }
+
+ public void handleDockDisconnected() {
+ // Update audio route states
+ AudioRoute dockRoute = mTypeRoutes.get(AudioRoute.TYPE_DOCK);
+ if (dockRoute != null) {
+ updateAvailableRoutes(dockRoute, false);
+ mSpeakerDockRoute = null;
+ }
+ AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER);
+ if (speakerRoute != null) {
+ updateAvailableRoutes(speakerRoute, true);
+ mSpeakerDockRoute = speakerRoute;
+ }
+ onAvailableRoutesChanged();
+
+ // Route to expected state
+ if (mCurrentRoute.equals(dockRoute)) {
+ routeTo(mIsActive, getBaseRoute(true, null));
+ }
+ }
+
+ private void handleStreamingEnabled() {
+ if (!mCurrentRoute.equals(mStreamingRoute)) {
+ routeTo(mIsActive, mStreamingRoute);
+ } else {
+ Log.i(this, "ignore enable streaming, already in streaming");
+ }
+ }
+
+ private void handleStreamingDisabled() {
+ if (mCurrentRoute.equals(mStreamingRoute)) {
+ mCurrentRoute = DUMMY_ROUTE;
+ onAvailableRoutesChanged();
+ 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(new Pair<>(BT_AUDIO_CONNECTED,
+ bluetoothDevice.getAddress()), null);
+ }
+ }
+ }
+
+ /**
+ * 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(new Pair<>(BT_AUDIO_DISCONNECTED,
+ bluetoothDevice.getAddress()), null);
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ 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:"
+ + AudioRoute.DEVICE_TYPE_STRINGS.get(type));
+ } else {
+ Log.i(this, "bluetooth route added: " + bluetoothRoute);
+ updateAvailableRoutes(bluetoothRoute, true);
+ mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice);
+ onAvailableRoutesChanged();
+ }
+ }
+
+ /**
+ * 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
+ AudioRoute bluetoothRoute = getBluetoothRoute(type, bluetoothDevice.getAddress());
+ if (bluetoothRoute != null) {
+ Log.i(this, "bluetooth route removed: " + bluetoothRoute);
+ mBluetoothRoutes.remove(bluetoothRoute);
+ updateAvailableRoutes(bluetoothRoute, false);
+ onAvailableRoutesChanged();
+ }
+
+ // Fallback to an available route
+ if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
+ 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,
+ 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) {
+ // Determine what the active device for the BT audio type was so that we can exclude this
+ // device from being used when calculating the base route.
+ String previouslyActiveDeviceAddress = mFeatureFlags
+ .resolveActiveBtRoutingAndBtTimingIssue()
+ ? mActiveDeviceCache.get(type)
+ : null;
+ // It's possible that the dest route hasn't been set yet when the controller is first
+ // initialized.
+ boolean pendingRouteNeedsUpdate = mPendingAudioRoute.getDestRoute() != null
+ && mPendingAudioRoute.getDestRoute().getType() == type;
+ boolean currentRouteNeedsUpdate = mCurrentRoute.getType() == type;
+ if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ if (pendingRouteNeedsUpdate) {
+ pendingRouteNeedsUpdate = mPendingAudioRoute.getDestRoute().getBluetoothAddress()
+ .equals(previouslyActiveDeviceAddress);
+ }
+ if (currentRouteNeedsUpdate) {
+ currentRouteNeedsUpdate = mCurrentRoute.getBluetoothAddress()
+ .equals(previouslyActiveDeviceAddress);
+ }
+ }
+ if ((mIsPending && pendingRouteNeedsUpdate) || (!mIsPending && currentRouteNeedsUpdate)) {
+ // Fallback to an available route excluding the previously active device.
+ routeTo(mIsActive, getBaseRoute(true, previouslyActiveDeviceAddress));
+ }
+ }
+
+ private void handleMuteChanged(boolean mute) {
+ mIsMute = mute;
+ if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
+ IAudioService audioService = mAudioServiceFactory.getAudioService();
+ Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
+ audioService == null);
+ if (audioService != null) {
+ try {
+ audioService.setMicrophoneMute(mute, mContext.getOpPackageName(),
+ mCallsManager.getCurrentUserHandle().getIdentifier(),
+ mContext.getAttributionTag());
+ } catch (RemoteException e) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+ ErrorStats.ERROR_EXTERNAL_EXCEPTION);
+ }
+ Log.e(this, e, "Remote exception while toggling mute.");
+ return;
+ }
+ }
+ }
+ onMuteStateChanged(mIsMute);
+ }
+
+ private void handleSwitchFocus(int focus, int handleEndTone) {
+ Log.i(this, "handleSwitchFocus: focus (%s)", focus);
+ mFocusType = focus;
+ switch (focus) {
+ case NO_FOCUS -> {
+ // Notify the CallAudioModeStateMachine that audio operations are complete so
+ // that we can relinquish audio focus.
+ mCallAudioManager.notifyAudioOperationsComplete();
+ // Reset mute state after call ends. This should remain unaffected if audio routing
+ // never went active.
+ handleMuteChanged(false);
+ // Ensure we reset call audio state at the end of the call (i.e. if we're on
+ // speaker, route back to earpiece). If we're on BT, remain on BT if it's still
+ // connected.
+ AudioRoute route = mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()
+ ? calculateBaselineRoute(false, true, null)
+ : mCurrentRoute;
+ routeTo(false, route);
+ // Clear pending messages
+ mPendingAudioRoute.clearPendingMessages();
+ clearRingingBluetoothAddress();
+ }
+ case ACTIVE_FOCUS -> {
+ // Route to active baseline route (we may need to change audio route in the case
+ // when a video call is put on hold). Ignore route changes if we're handling playing
+ // the end tone. Otherwise, it's possible that we'll override the route a client has
+ // previously requested.
+ if (handleEndTone == 0) {
+ // Cache BT device switch in the case that inband ringing is disabled and audio
+ // was routed to a watch. When active focus is received, this selection will be
+ // honored provided that the current route is associated.
+ Log.i(this, "handleSwitchFocus (ACTIVE_FOCUS): mBluetoothAddressForRinging = "
+ + "%s, mCurrentRoute = %s", mBluetoothAddressForRinging, mCurrentRoute);
+ AudioRoute audioRoute = mBluetoothAddressForRinging != null
+ && mBluetoothAddressForRinging.equals(
+ mCurrentRoute.getBluetoothAddress())
+ ? mCurrentRoute
+ : getBaseRoute(true, null);
+ routeTo(true, audioRoute);
+ clearRingingBluetoothAddress();
+ }
+ }
+ case RINGING_FOCUS -> {
+ if (!mIsActive) {
+ 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 {
+ // Route is already active.
+ BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
+ if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+ routeTo(false, mCurrentRoute);
+ }
+ }
+ }
+ }
+ }
+
+ public void handleSwitchEarpiece() {
+ AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+ if (earpieceRoute != null && getCallSupportedRoutes().contains(earpieceRoute)) {
+ routeTo(mIsActive, earpieceRoute);
+ } else {
+ Log.i(this, "ignore switch earpiece request");
+ }
+ }
+
+ private void handleSwitchBluetooth(String address) {
+ Log.i(this, "handle switch to bluetooth with address %s", address);
+ AudioRoute bluetoothRoute = null;
+ BluetoothDevice bluetoothDevice = null;
+ if (address == null) {
+ bluetoothRoute = getArbitraryBluetoothDevice();
+ bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute);
+ } else {
+ for (AudioRoute route : getCallSupportedRoutes()) {
+ if (Objects.equals(address, route.getBluetoothAddress())) {
+ bluetoothRoute = route;
+ bluetoothDevice = mBluetoothRoutes.get(route);
+ break;
+ }
+ }
+ }
+
+ if (bluetoothRoute != null && bluetoothDevice != null) {
+ if (mFocusType == RINGING_FOCUS) {
+ routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
+ bluetoothRoute);
+ mBluetoothAddressForRinging = bluetoothDevice.getAddress();
+ } else {
+ routeTo(mIsActive, bluetoothRoute);
+ }
+ } else {
+ Log.i(this, "ignore switch bluetooth request to unavailable address");
+ }
+ }
+
+ /**
+ * Retrieve the active BT device, if available, otherwise return the most recently tracked
+ * active device, or null if none are available.
+ * @return {@link AudioRoute} of the BT device.
+ */
+ private AudioRoute getArbitraryBluetoothDevice() {
+ if (mActiveBluetoothDevice != null) {
+ return getBluetoothRoute(mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
+ } else if (!mBluetoothRoutes.isEmpty()) {
+ return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+ }
+ return null;
+ }
+
+ private void handleSwitchHeadset() {
+ AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
+ if (headsetRoute != null && getCallSupportedRoutes().contains(headsetRoute)) {
+ routeTo(mIsActive, headsetRoute);
+ } else {
+ Log.i(this, "ignore switch headset request");
+ }
+ }
+
+ private void handleSwitchSpeaker() {
+ if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)
+ && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER) {
+ routeTo(mIsActive, mSpeakerDockRoute);
+ } else {
+ Log.i(this, "ignore switch speaker request");
+ }
+ }
+
+ private void handleSwitchBaselineRoute(boolean isExplicitUserRequest, boolean includeBluetooth,
+ String btAddressToExclude) {
+ Log.i(this, "handleSwitchBaselineRoute: includeBluetooth: %b, "
+ + "btAddressToExclude: %s", includeBluetooth, btAddressToExclude);
+ boolean areExcludedBtAndDestBtSame = btAddressToExclude != null
+ && mPendingAudioRoute.getDestRoute() != null
+ && Objects.equals(btAddressToExclude, mPendingAudioRoute.getDestRoute()
+ .getBluetoothAddress());
+ Pair<Integer, String> btDevicePendingMsg =
+ new Pair<>(BT_AUDIO_CONNECTED, btAddressToExclude);
+
+ // If SCO is once again connected or there's a pending message for BT_AUDIO_CONNECTED, then
+ // we know that the device has reconnected or is in the middle of connecting. Ignore routing
+ // out of this BT device.
+ boolean isExcludedDeviceConnectingOrConnected = areExcludedBtAndDestBtSame
+ && (mIsScoAudioConnected || mPendingAudioRoute.getPendingMessages()
+ .contains(btDevicePendingMsg));
+ // Check if the pending audio route or current route is already different from the route
+ // including the BT device that should be excluded from route selection.
+ boolean isCurrentOrDestRouteDifferent = btAddressToExclude != null
+ && ((mIsPending && !btAddressToExclude.equals(mPendingAudioRoute.getDestRoute()
+ .getBluetoothAddress())) || (!mIsPending && !btAddressToExclude.equals(
+ mCurrentRoute.getBluetoothAddress())));
+ if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ if (isExcludedDeviceConnectingOrConnected) {
+ Log.i(this, "BT device with address (%s) is currently connecting/connected. "
+ + "Ignoring route switch.", btAddressToExclude);
+ return;
+ } else if (isCurrentOrDestRouteDifferent) {
+ Log.i(this, "Current or pending audio route isn't routed to device with address "
+ + "(%s). Ignoring route switch.", btAddressToExclude);
+ return;
+ }
+ }
+ routeTo(mIsActive, calculateBaselineRoute(isExplicitUserRequest, includeBluetooth,
+ btAddressToExclude));
+ }
+
+ private void handleSpeakerOn() {
+ if (isPending()) {
+ 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 && getCallSupportedRoutes().contains(mSpeakerDockRoute)
+ && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER
+ && mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+ routeTo(mIsActive, mSpeakerDockRoute);
+ // 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) {
+ sendMessageWithSessionInfo(SPEAKER_ON);
+ }
+ }
+ }
+ }
+
+ private void handleSpeakerOff() {
+ if (isPending()) {
+ 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, 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) {
+ sendMessageWithSessionInfo(SPEAKER_OFF);
+ }
+ onAvailableRoutesChanged();
+ }
+ }
+
+ /**
+ * 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) {
+ mCurrentRoute = mPendingAudioRoute.getDestRoute();
+ Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
+ "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
+ mIsPending = false;
+ mPendingAudioRoute.clearPendingMessages();
+ onCurrentRouteChanged();
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, true);
+ }
+ }
+ }
+
+ private void onCurrentRouteChanged() {
+ synchronized (mLock) {
+ BluetoothDevice activeBluetoothDevice = null;
+ int route = ROUTE_MAP.get(mCurrentRoute.getType());
+ if (route == CallAudioState.ROUTE_STREAMING) {
+ updateCallAudioState(new CallAudioState(mIsMute, route, route));
+ return;
+ }
+ if (route == CallAudioState.ROUTE_BLUETOOTH) {
+ activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+ }
+ updateCallAudioState(new CallAudioState(mIsMute, route,
+ mCallAudioState.getRawSupportedRouteMask(), activeBluetoothDevice,
+ mCallAudioState.getSupportedBluetoothDevices()));
+ }
+ }
+
+ private void onAvailableRoutesChanged() {
+ synchronized (mLock) {
+ int routeMask = 0;
+ Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
+ for (AudioRoute route : getCallSupportedRoutes()) {
+ routeMask |= ROUTE_MAP.get(route.getType());
+ if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
+ 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 (deviceToAdd != null && 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,
+ mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
+ }
+ }
+
+ private void onMuteStateChanged(boolean mute) {
+ updateCallAudioState(new CallAudioState(mute, mCallAudioState.getRoute(),
+ mCallAudioState.getSupportedRouteMask(), mCallAudioState.getActiveBluetoothDevice(),
+ mCallAudioState.getSupportedBluetoothDevices()));
+ }
+
+ /**
+ * Retrieves the current call's supported audio route and adjusts the audio routing if the
+ * current route isn't supported.
+ */
+ private void updateRouteForForeground() {
+ boolean updatedRouteForCall = updateCallSupportedAudioRoutes();
+ // Ensure that current call audio state has updated routes for current call.
+ if (updatedRouteForCall) {
+ mCallAudioState = new CallAudioState(mIsMute, mCallAudioState.getRoute(),
+ mCallSupportedRouteMask, mCallAudioState.getActiveBluetoothDevice(),
+ mCallAudioState.getSupportedBluetoothDevices());
+ // Update audio route if foreground call doesn't support the current route.
+ if ((mCallSupportedRouteMask & mCallAudioState.getRoute()) == 0) {
+ routeTo(mIsActive, getBaseRoute(true, null));
+ }
+ }
+ }
+
+ /**
+ * Update supported audio routes for the foreground call if present.
+ */
+ private boolean updateCallSupportedAudioRoutes() {
+ int availableRouteMask = 0;
+ Call foregroundCall = mCallsManager.getForegroundCall();
+ mCallSupportedRoutes.clear();
+ if (foregroundCall != null) {
+ int foregroundCallSupportedRouteMask = foregroundCall.getSupportedAudioRoutes();
+ for (AudioRoute route : getAvailableRoutes()) {
+ int routeType = ROUTE_MAP.get(route.getType());
+ availableRouteMask |= routeType;
+ if ((routeType & foregroundCallSupportedRouteMask) == routeType) {
+ mCallSupportedRoutes.add(route);
+ }
+ }
+ mCallSupportedRouteMask = availableRouteMask & foregroundCallSupportedRouteMask;
+ return true;
+ } else {
+ mCallSupportedRouteMask = -1;
+ return false;
+ }
+ }
+
+ private void updateCallAudioState(CallAudioState newCallAudioState) {
+ synchronized (mTelecomLock) {
+ Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
+ CallAudioState oldState = mCallAudioState;
+ mCallAudioState = newCallAudioState;
+ // Update status bar notification
+ mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
+ mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
+ updateAudioStateForTrackedCalls(mCallAudioState);
+ }
+ }
+
+ private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) {
+ List<Call> calls = new ArrayList<>(mCallsManager.getTrackedCalls());
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ }
+ }
+ }
+
+ private AudioRoute getPreferredAudioRouteFromStrategy() {
+ // Get preferred device
+ AudioDeviceAttributes deviceAttr = getPreferredDeviceForStrategy();
+ Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
+ if (deviceAttr == null) {
+ return null;
+ }
+
+ // Get corresponding audio route
+ @AudioRoute.AudioRouteType int type = DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(
+ deviceAttr.getType());
+ if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
+ return getBluetoothRoute(type, deviceAttr.getAddress());
+ } else {
+ return mTypeRoutes.get(type);
+ }
+ }
+
+ private AudioDeviceAttributes getPreferredDeviceForStrategy() {
+ // Get audio produce strategy
+ AudioProductStrategy strategy = null;
+ final AudioAttributes attr = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ .build();
+ List<AudioProductStrategy> strategies = AudioManager.getAudioProductStrategies();
+ for (AudioProductStrategy s : strategies) {
+ if (s.supportsAudioAttributes(attr)) {
+ strategy = s;
+ }
+ }
+ if (strategy == null) {
+ return null;
+ }
+
+ return mAudioManager.getPreferredDeviceForStrategy(strategy);
+ }
+
+ private AudioRoute getPreferredAudioRouteFromDefault(boolean isExplicitUserRequest,
+ boolean includeBluetooth, String btAddressToExclude) {
+ boolean skipEarpiece = false;
+ Call foregroundCall = mCallAudioManager.getForegroundCall();
+ if (!mFeatureFlags.fixUserRequestBaselineRouteVideoCall()) {
+ isExplicitUserRequest = false;
+ }
+ if (!isExplicitUserRequest) {
+ 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 ((!mCallSupportedRoutes.isEmpty() && (mCallSupportedRouteMask
+ & CallAudioState.ROUTE_BLUETOOTH) == 0) || mBluetoothRoutes.isEmpty()
+ || !includeBluetooth || activeWatchOrNonWatchDeviceRoute == null) {
+ Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+ + "available non-BT route.");
+ boolean callSupportsEarpieceWiredRoute = mCallSupportedRoutes.isEmpty()
+ || mCallSupportedRoutes.contains(mEarpieceWiredRoute);
+ // If call supported route doesn't contain earpiece/wired/BT, it should have speaker
+ // enabled. Otherwise, no routes would be supported for the call which should never be
+ // the case.
+ AudioRoute defaultRoute = mEarpieceWiredRoute != null && callSupportsEarpieceWiredRoute
+ ? 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 != null
+ && 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 (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 calculateSupportedRouteMaskInit() {
+ Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s",
+ mWiredHeadsetManager.isPluggedIn());
+ int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ AudioDeviceInfo[] deviceList = mAudioManager.getDevices(
+ AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device: deviceList) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
+ routeMask |= CallAudioState.ROUTE_EARPIECE;
+ break;
+ }
+ }
+ }
+ return routeMask;
+ }
+
+ @VisibleForTesting
+ public Set<AudioRoute> getAvailableRoutes() {
+ if (mCurrentRoute.equals(mStreamingRoute)) {
+ return mStreamingRoutes;
+ } else {
+ return mAvailableRoutes;
+ }
+ }
+
+ public Set<AudioRoute> getCallSupportedRoutes() {
+ if (mCurrentRoute.equals(mStreamingRoute)) {
+ return mStreamingRoutes;
+ } else {
+ if (mAvailableRoutesUpdated) {
+ updateCallSupportedAudioRoutes();
+ mAvailableRoutesUpdated = false;
+ }
+ return mCallSupportedRoutes.isEmpty() ? mAvailableRoutes : mCallSupportedRoutes;
+ }
+ }
+
+ public AudioRoute getCurrentRoute() {
+ return mCurrentRoute;
+ }
+
+ public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+ String address) {
+ for (AudioRoute route : mBluetoothRoutes.keySet()) {
+ if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
+ return route;
+ }
+ }
+ return null;
+ }
+
+ public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
+ AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+ Log.i(this, "getBaseRoute: preferred audio route is %s", destRoute);
+ if (destRoute == null || (destRoute.getBluetoothAddress() != null && (!includeBluetooth
+ || destRoute.getBluetoothAddress().equals(btAddressToExclude)))) {
+ destRoute = getPreferredAudioRouteFromDefault(false, includeBluetooth, btAddressToExclude);
+ }
+ if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) {
+ destRoute = null;
+ }
+ Log.i(this, "getBaseRoute - audio routing to %s", destRoute);
+ return destRoute;
+ }
+
+ private AudioRoute calculateBaselineRoute(boolean isExplicitUserRequest,
+ boolean includeBluetooth, String btAddressToExclude) {
+ AudioRoute destRoute = getPreferredAudioRouteFromDefault(isExplicitUserRequest,
+ includeBluetooth, btAddressToExclude);
+ if (destRoute != null && !getCallSupportedRoutes().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 = getAvailableBluetoothDevicesForRouting();
+ // 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.
+ boolean isActiveDevice = mActiveBluetoothDevice != null
+ && device.getAddress().equals(mActiveBluetoothDevice.second);
+ if (i == (bluetoothRoutes.size() - 1) && mBluetoothRouteManager.isWatch(device)
+ && (device.equals(mCallAudioState.getActiveBluetoothDevice())
+ || isActiveDevice)) {
+ 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 = route;
+ break;
+ }
+ }
+
+ Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute);
+ return nonWatchDeviceRoute;
+ }
+
+ private List<AudioRoute> getAvailableBluetoothDevicesForRouting() {
+ List<AudioRoute> bluetoothRoutes = new ArrayList<>(mBluetoothRoutes.keySet());
+ if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ return bluetoothRoutes;
+ }
+ // Consider the active device (BT_ACTIVE_DEVICE_PRESENT) if it exists first.
+ AudioRoute activeDeviceRoute = getArbitraryBluetoothDevice();
+ if (activeDeviceRoute != null && (bluetoothRoutes.isEmpty()
+ || !bluetoothRoutes.get(bluetoothRoutes.size() - 1).equals(activeDeviceRoute))) {
+ Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: active BT device (%s) present."
+ + "Considering this device for selection first.", activeDeviceRoute);
+ bluetoothRoutes.add(activeDeviceRoute);
+ }
+ return bluetoothRoutes;
+ }
+
+ /**
+ * 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;
+ }
+
+ @VisibleForTesting
+ public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) {
+ mAudioRouteFactory = audioRouteFactory;
+ }
+
+ public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+ return mBluetoothRoutes;
+ }
+
+ public void overrideIsPending(boolean isPending) {
+ mIsPending = isPending;
+ }
+
+ public void setIsScoAudioConnected(boolean value) {
+ mIsScoAudioConnected = value;
+ }
+
+ private void clearRingingBluetoothAddress() {
+ mBluetoothAddressForRinging = null;
+ }
+
+ /**
+ * Update the active bluetooth device being tracked (as well as for individual profiles).
+ * We need to keep track of active devices for individual profiles because of potential
+ * inconsistencies found in BluetoothStateReceiver#handleActiveDeviceChanged. When multiple
+ * profiles are paired, we could have a scenario where an active device A is replaced
+ * with an active device B (from a different profile), which is then removed as an active
+ * device shortly after, causing device A to be reactive. It's possible that the active device
+ * changed intent is never received again for device A so an active device cache is necessary
+ * to track these devices at a profile level.
+ * @param device {@link Pair} containing the BT audio route type (i.e. SCO/HA/LE) and the
+ * address of the device.
+ */
+ public void updateActiveBluetoothDevice(Pair<Integer, String> device) {
+ mActiveDeviceCache.put(device.first, device.second);
+ // Update most recently active device if address isn't null (meaning some device is active).
+ if (device.second != null) {
+ mActiveBluetoothDevice = device;
+ } else {
+ // If a device was removed, check to ensure that no other device is still considered
+ // active.
+ boolean hasActiveDevice = false;
+ List<Map.Entry<Integer, String>> activeBtDevices = new ArrayList<>(
+ mActiveDeviceCache.entrySet());
+ for (Map.Entry<Integer,String> activeDevice : activeBtDevices) {
+ Integer btAudioType = activeDevice.getKey();
+ String address = activeDevice.getValue();
+ if (address != null) {
+ hasActiveDevice = true;
+ if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ mActiveBluetoothDevice = new Pair<>(btAudioType, address);
+ }
+ break;
+ }
+ }
+ if (!hasActiveDevice) {
+ mActiveBluetoothDevice = null;
+ }
+ }
+ }
+
+ private void updateAvailableRoutes(AudioRoute route, boolean includeRoute) {
+ if (includeRoute) {
+ mAvailableRoutes.add(route);
+ } else {
+ mAvailableRoutes.remove(route);
+ }
+ mAvailableRoutesUpdated = true;
+ }
+
+ @VisibleForTesting
+ public void setActive(boolean active) {
+ if (active) {
+ mFocusType = ACTIVE_FOCUS;
+ } else {
+ mFocusType = NO_FOCUS;
+ }
+ mIsActive = active;
+ }
+
+ void fallBack(String btAddressToExclude) {
+ mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, false);
+ sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE,
+ btAddressToExclude);
+ }
+}
diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
index af0757c..8a87c22 100644
--- a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -25,17 +25,17 @@
public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.Listener,
DockManager.Listener, BluetoothRouteManager.BluetoothStateListener {
- private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ private final CallAudioRouteAdapter mCallAudioAdapter;
private final BluetoothRouteManager mBluetoothRouteManager;
private final AsyncRingtonePlayer mRingtonePlayer;
public CallAudioRoutePeripheralAdapter(
- CallAudioRouteStateMachine callAudioRouteStateMachine,
+ CallAudioRouteAdapter callAudioRouteAdapter,
BluetoothRouteManager bluetoothManager,
WiredHeadsetManager wiredHeadsetManager,
DockManager dockManager,
AsyncRingtonePlayer ringtonePlayer) {
- mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+ mCallAudioAdapter = callAudioRouteAdapter;
mBluetoothRouteManager = bluetoothManager;
mRingtonePlayer = ringtonePlayer;
@@ -60,26 +60,26 @@
@Override
public void onBluetoothDeviceListChanged() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED);
}
@Override
public void onBluetoothActiveDevicePresent() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
}
@Override
public void onBluetoothActiveDeviceGone() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_GONE);
}
@Override
public void onBluetoothAudioConnected() {
mRingtonePlayer.updateBtActiveState(true);
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
}
@@ -87,20 +87,20 @@
public void onBluetoothAudioConnecting() {
mRingtonePlayer.updateBtActiveState(false);
// Pretend like audio is connected when communicating w/ CARSM.
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
}
@Override
public void onBluetoothAudioDisconnected() {
mRingtonePlayer.updateBtActiveState(false);
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
}
@Override
public void onUnexpectedBluetoothStateChange() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
}
@@ -111,17 +111,17 @@
@Override
public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
if (!oldIsPluggedIn && newIsPluggedIn) {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
} else if (oldIsPluggedIn && !newIsPluggedIn){
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
}
}
@Override
public void onDockChanged(boolean isDocked) {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
isDocked ? CallAudioRouteStateMachine.CONNECT_DOCK
: CallAudioRouteStateMachine.DISCONNECT_DOCK
);
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 46743be..4283b7b 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -28,6 +28,7 @@
import android.media.AudioManager;
import android.media.IAudioService;
import android.os.Binder;
+import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
@@ -44,6 +45,7 @@
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.Collection;
import java.util.HashMap;
@@ -72,7 +74,7 @@
* from a wired headset
* mIsMuted: a boolean indicating whether the audio is muted
*/
-public class CallAudioRouteStateMachine extends StateMachine {
+public class CallAudioRouteStateMachine extends StateMachine implements CallAudioRouteAdapter {
public static class Factory {
public CallAudioRouteStateMachine create(
@@ -83,7 +85,9 @@
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
int earpieceControl,
- Executor asyncTaskExecutor) {
+ Executor asyncTaskExecutor,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
return new CallAudioRouteStateMachine(context,
callsManager,
bluetoothManager,
@@ -91,7 +95,9 @@
statusBarNotifier,
audioServiceFactory,
earpieceControl,
- asyncTaskExecutor);
+ asyncTaskExecutor,
+ communicationDeviceTracker,
+ featureFlags);
}
}
/** Values for CallAudioRouteStateMachine constructor's earPieceRouting arg. */
@@ -114,63 +120,6 @@
/** Direct the audio stream through another device. */
public static final int ROUTE_STREAMING = CallAudioState.ROUTE_STREAMING;
- /** Valid values for msg.what */
- public static final int CONNECT_WIRED_HEADSET = 1;
- public static final int DISCONNECT_WIRED_HEADSET = 2;
- public static final int CONNECT_DOCK = 5;
- public static final int DISCONNECT_DOCK = 6;
- public static final int BLUETOOTH_DEVICE_LIST_CHANGED = 7;
- public static final int BT_ACTIVE_DEVICE_PRESENT = 8;
- public static final int BT_ACTIVE_DEVICE_GONE = 9;
-
- public static final int SWITCH_EARPIECE = 1001;
- public static final int SWITCH_BLUETOOTH = 1002;
- public static final int SWITCH_HEADSET = 1003;
- public static final int SWITCH_SPEAKER = 1004;
- // Wired headset, earpiece, or speakerphone, in that order of precedence.
- public static final int SWITCH_BASELINE_ROUTE = 1005;
-
- // Messages denoting that the speakerphone was turned on/off. Used to update state when we
- // weren't the ones who turned it on/off
- public static final int SPEAKER_ON = 1006;
- public static final int SPEAKER_OFF = 1007;
-
- // Messages denoting that the streaming route switch request was sent.
- public static final int STREAMING_FORCE_ENABLED = 1008;
- public static final int STREAMING_FORCE_DISABLED = 1009;
-
- public static final int USER_SWITCH_EARPIECE = 1101;
- public static final int USER_SWITCH_BLUETOOTH = 1102;
- public static final int USER_SWITCH_HEADSET = 1103;
- public static final int USER_SWITCH_SPEAKER = 1104;
- public static final int USER_SWITCH_BASELINE_ROUTE = 1105;
-
- public static final int UPDATE_SYSTEM_AUDIO_ROUTE = 1201;
-
- // These three messages indicate state changes that come from BluetoothRouteManager.
- // They may be triggered by the BT stack doing something on its own or they may be sent after
- // we request that the BT stack do something. Any logic for these messages should take into
- // account the possibility that the event indicated has already been processed (i.e. handling
- // should be idempotent).
- public static final int BT_AUDIO_DISCONNECTED = 1301;
- public static final int BT_AUDIO_CONNECTED = 1302;
- public static final int BT_AUDIO_PENDING = 1303;
-
- public static final int MUTE_ON = 3001;
- public static final int MUTE_OFF = 3002;
- public static final int TOGGLE_MUTE = 3003;
- public static final int MUTE_EXTERNALLY_CHANGED = 3004;
-
- public static final int SWITCH_FOCUS = 4001;
-
- // Used in testing to execute verifications. Not compatible with subsessions.
- public static final int RUN_RUNNABLE = 9001;
-
- /** Valid values for mAudioFocusType */
- public static final int NO_FOCUS = 1;
- public static final int ACTIVE_FOCUS = 2;
- public static final int RINGING_FOCUS = 3;
-
/** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
public static final int NO_INCLUDE_BLUETOOTH_IN_BASELINE = 0;
public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
@@ -183,45 +132,6 @@
put(CallAudioState.ROUTE_WIRED_HEADSET, LogUtils.Events.AUDIO_ROUTE_HEADSET);
}};
- private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
- put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
- put(DISCONNECT_WIRED_HEADSET, "DISCONNECT_WIRED_HEADSET");
- put(CONNECT_DOCK, "CONNECT_DOCK");
- put(DISCONNECT_DOCK, "DISCONNECT_DOCK");
- put(BLUETOOTH_DEVICE_LIST_CHANGED, "BLUETOOTH_DEVICE_LIST_CHANGED");
- put(BT_ACTIVE_DEVICE_PRESENT, "BT_ACTIVE_DEVICE_PRESENT");
- put(BT_ACTIVE_DEVICE_GONE, "BT_ACTIVE_DEVICE_GONE");
-
- put(SWITCH_EARPIECE, "SWITCH_EARPIECE");
- put(SWITCH_BLUETOOTH, "SWITCH_BLUETOOTH");
- put(SWITCH_HEADSET, "SWITCH_HEADSET");
- put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
- put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
- put(SPEAKER_ON, "SPEAKER_ON");
- put(SPEAKER_OFF, "SPEAKER_OFF");
-
- put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
- put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
- put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
- put(USER_SWITCH_SPEAKER, "USER_SWITCH_SPEAKER");
- put(USER_SWITCH_BASELINE_ROUTE, "USER_SWITCH_BASELINE_ROUTE");
-
- put(UPDATE_SYSTEM_AUDIO_ROUTE, "UPDATE_SYSTEM_AUDIO_ROUTE");
-
- put(BT_AUDIO_DISCONNECTED, "BT_AUDIO_DISCONNECTED");
- put(BT_AUDIO_CONNECTED, "BT_AUDIO_CONNECTED");
- put(BT_AUDIO_PENDING, "BT_AUDIO_PENDING");
-
- put(MUTE_ON, "MUTE_ON");
- put(MUTE_OFF, "MUTE_OFF");
- put(TOGGLE_MUTE, "TOGGLE_MUTE");
- put(MUTE_EXTERNALLY_CHANGED, "MUTE_EXTERNALLY_CHANGED");
-
- put(SWITCH_FOCUS, "SWITCH_FOCUS");
-
- put(RUN_RUNNABLE, "RUN_RUNNABLE");
- }};
-
private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
@@ -371,11 +281,20 @@
public void enter() {
super.enter();
setSpeakerphoneOn(false);
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, null);
+ }
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
@@ -401,6 +320,10 @@
case SWITCH_BLUETOOTH:
case USER_SWITCH_BLUETOOTH:
if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ }
if (mAudioFocusType == ACTIVE_FOCUS
|| mBluetoothRouteManager.isInbandRingingEnabled()) {
String address = (msg.obj instanceof SomeArgs) ?
@@ -417,6 +340,10 @@
case SWITCH_HEADSET:
case USER_SWITCH_HEADSET:
if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ }
transitionTo(mActiveHeadsetRoute);
} else {
Log.w(this, "Ignoring switch to headset command. Not available.");
@@ -426,6 +353,10 @@
// fall through; we want to switch to speaker mode when docked and in a call.
case SWITCH_SPEAKER:
case USER_SWITCH_SPEAKER:
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ }
setSpeakerphoneOn(true);
// fall through
case SPEAKER_ON:
@@ -579,10 +510,19 @@
public void enter() {
super.enter();
setSpeakerphoneOn(false);
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_WIRED_HEADSET, null);
+ }
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
- setSystemAudioState(newState, true);
- updateInternalCallAudioState();
+ if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+ updateInternalCallAudioState();
+ setSystemAudioState(newState, true);
+ } else {
+ setSystemAudioState(newState, true);
+ updateInternalCallAudioState();
+ }
}
@Override
@@ -600,6 +540,10 @@
case SWITCH_EARPIECE:
case USER_SWITCH_EARPIECE:
if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ }
transitionTo(mActiveEarpieceRoute);
} else {
Log.w(this, "Ignoring switch to earpiece command. Not available.");
@@ -615,6 +559,10 @@
|| mBluetoothRouteManager.isInbandRingingEnabled()) {
String address = (msg.obj instanceof SomeArgs) ?
(String) ((SomeArgs) msg.obj).arg2 : null;
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ }
// Omit transition to ActiveBluetoothRoute until actual connection.
setBluetoothOn(address);
} else {
@@ -631,6 +579,10 @@
return HANDLED;
case SWITCH_SPEAKER:
case USER_SWITCH_SPEAKER:
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ }
setSpeakerphoneOn(true);
// fall through
case SPEAKER_ON:
@@ -793,11 +745,27 @@
public void enter() {
super.enter();
setSpeakerphoneOn(false);
+ // Try arbitrarily connecting to BT audio if we haven't already. This handles
+ // the edge case of when the audio route is in a quiescent route while in-call and
+ // the BT connection fails to be set. Previously, the logic was to setBluetoothOn in
+ // ACTIVE_FOCUS but the route would still remain in a quiescent route, so instead we
+ // should be transitioning directly into the active route.
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ setBluetoothOn(null);
+ }
+ if (mFeatureFlags.updateRouteMaskWhenBtConnected()) {
+ mAvailableRoutes |= ROUTE_BLUETOOTH;
+ }
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();
@@ -893,8 +861,21 @@
case SWITCH_FOCUS:
if (msg.arg1 == NO_FOCUS) {
// Only disconnect audio here instead of routing away from BT entirely.
- mBluetoothRouteManager.disconnectAudio();
- reinitialize();
+ 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 {
+ mBluetoothRouteManager.disconnectAudio();
+ reinitialize();
+ }
mCallAudioManager.notifyAudioOperationsComplete();
} else if (msg.arg1 == RINGING_FOCUS
&& !mBluetoothRouteManager.isInbandRingingEnabled()) {
@@ -932,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
@@ -1065,7 +1051,13 @@
return HANDLED;
case SWITCH_FOCUS:
if (msg.arg1 == ACTIVE_FOCUS) {
- setBluetoothOn(null);
+ // It is possible that the connection to BT will fail while in-call, in
+ // which case, we want to transition into the active route.
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ setBluetoothOn(null);
+ }
} else if (msg.arg1 == RINGING_FOCUS) {
if (mBluetoothRouteManager.isInbandRingingEnabled()) {
setBluetoothOn(null);
@@ -1150,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
@@ -1520,6 +1517,8 @@
private CallAudioState mLastKnownCallAudioState;
private CallAudioManager mCallAudioManager;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private FeatureFlags mFeatureFlags;
public CallAudioRouteStateMachine(
Context context,
@@ -1529,7 +1528,9 @@
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
int earpieceControl,
- Executor asyncTaskExecutor) {
+ Executor asyncTaskExecutor,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
super(NAME);
mContext = context;
mCallsManager = callsManager;
@@ -1540,6 +1541,8 @@
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
mAsyncTaskExecutor = asyncTaskExecutor;
+ mCommunicationDeviceTracker = communicationDeviceTracker;
+ mFeatureFlags = featureFlags;
createStates(earpieceControl);
}
@@ -1551,7 +1554,9 @@
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl, Looper looper, Executor asyncTaskExecutor) {
+ int earpieceControl, Looper looper, Executor asyncTaskExecutor,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
super(NAME, looper);
mContext = context;
mCallsManager = callsManager;
@@ -1562,7 +1567,8 @@
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
mAsyncTaskExecutor = asyncTaskExecutor;
-
+ mCommunicationDeviceTracker = communicationDeviceTracker;
+ mFeatureFlags = featureFlags;
createStates(earpieceControl);
}
@@ -1665,11 +1671,11 @@
}
public void sendMessageWithSessionInfo(int message, int arg) {
- sendMessageWithSessionInfo(message, arg, null);
+ sendMessageWithSessionInfo(message, arg, (String) null);
}
public void sendMessageWithSessionInfo(int message) {
- sendMessageWithSessionInfo(message, 0, null);
+ sendMessageWithSessionInfo(message, 0, (String) null);
}
public void sendMessageWithSessionInfo(int message, int arg, String data) {
@@ -1679,6 +1685,19 @@
sendMessage(message, arg, 0, args);
}
+ public void sendMessageWithSessionInfo(int message, int arg, int data) {
+ // ignore, only used in CallAudioRouteController
+ }
+
+ public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
+ // ignore, only used in CallAudioRouteController
+ }
+
+ @Override
+ public void sendMessage(int message, Runnable r) {
+ super.sendMessage(message, r);
+ }
+
/**
* This is for state-independent changes in audio route (i.e. muting or runnables)
* @param msg that couldn't be handled.
@@ -1708,9 +1727,19 @@
}
return;
case UPDATE_SYSTEM_AUDIO_ROUTE:
- updateInternalCallAudioState();
- updateRouteForForegroundCall();
- resendSystemAudioState();
+ if (mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()) {
+ // Ensure available routes is updated.
+ updateRouteForForegroundCall();
+ // Ensure current audio state gets updated to take this into account.
+ updateInternalCallAudioState();
+ // Either resend the current audio state as it stands, or update to reflect any
+ // changes put into place based on mAvailableRoutes
+ setSystemAudioState(mCurrentCallAudioState, true);
+ } else {
+ updateInternalCallAudioState();
+ updateRouteForForegroundCall();
+ resendSystemAudioState();
+ }
return;
case RUN_RUNNABLE:
java.lang.Runnable r = (java.lang.Runnable) msg.obj;
@@ -1735,7 +1764,7 @@
}
public void dumpPendingMessages(IndentingPrintWriter pw) {
- getHandler().getLooper().dump(pw::println, "");
+ getAdapterHandler().getLooper().dump(pw::println, "");
}
public boolean isHfpDeviceAvailable() {
@@ -1747,31 +1776,19 @@
final boolean hasAnyCalls = mCallsManager.hasAnyCalls();
// These APIs are all via two-way binder calls so can potentially block Telecom. Since none
// of this has to happen in the Telecom lock we'll offload it to the async executor.
-
- AudioDeviceInfo speakerDevice = null;
- for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
- if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- speakerDevice = info;
- break;
- }
- }
boolean speakerOn = false;
- if (speakerDevice != null && on) {
- boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
- if (result) {
- speakerOn = true;
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ if (on) {
+ speakerOn = mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
+ } else {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
}
} else {
- AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
- if (curDevice != null
- && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- mAudioManager.clearCommunicationDevice();
- }
+ speakerOn = processLegacySpeakerCommunicationDevice(on);
}
- final boolean isSpeakerOn = speakerOn;
- mAsyncTaskExecutor.execute(() -> {
- mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && isSpeakerOn);
- });
+ mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && speakerOn);
}
private void setBluetoothOn(String address) {
@@ -1869,6 +1886,11 @@
setSystemAudioState(mLastKnownCallAudioState, true);
}
+ @VisibleForTesting
+ public CallAudioState getLastKnownCallAudioState() {
+ return mLastKnownCallAudioState;
+ }
+
private void setSystemAudioState(CallAudioState newCallAudioState, boolean force) {
synchronized (mLock) {
Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState,
@@ -1987,6 +2009,11 @@
}
private boolean isWatchActiveOrOnlyWatchesAvailable() {
+ if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+ Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: Flag is disabled.");
+ return false;
+ }
+
boolean containsWatchDevice = false;
boolean containsNonWatchDevice = false;
Collection<BluetoothDevice> connectedBtDevices =
@@ -2009,6 +2036,30 @@
return containsWatchDevice && !containsNonWatchDevice && !isActiveDeviceWatch;
}
+ private boolean processLegacySpeakerCommunicationDevice(boolean on) {
+ AudioDeviceInfo speakerDevice = null;
+ for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
+ if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ speakerDevice = info;
+ break;
+ }
+ }
+ boolean speakerOn = false;
+ if (speakerDevice != null && on) {
+ boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
+ if (result) {
+ speakerOn = true;
+ }
+ } else {
+ AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
+ if (curDevice != null
+ && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ mAudioManager.clearCommunicationDevice();
+ }
+ }
+ return speakerOn;
+ }
+
private int calculateBaselineRouteMessage(boolean isExplicitUserRequest,
boolean includeBluetooth) {
boolean isSkipEarpiece = false;
@@ -2059,8 +2110,9 @@
private int getCurrentCallSupportedRoutes() {
int supportedRoutes = CallAudioState.ROUTE_ALL;
- if (mCallsManager.getForegroundCall() != null) {
- supportedRoutes &= mCallsManager.getForegroundCall().getSupportedAudioRoutes();
+ Call foregroundCall = mCallsManager.getForegroundCall();
+ if (foregroundCall != null) {
+ supportedRoutes &= foregroundCall.getSupportedAudioRoutes();
}
return supportedRoutes;
@@ -2077,4 +2129,15 @@
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/CallDiagnosticServiceController.java b/src/com/android/server/telecom/CallDiagnosticServiceController.java
index 6c7ee38..1077f0d 100644
--- a/src/com/android/server/telecom/CallDiagnosticServiceController.java
+++ b/src/com/android/server/telecom/CallDiagnosticServiceController.java
@@ -522,7 +522,7 @@
callId, messageId, message);
if (mPlayerFactory != null) {
// Play that tone!
- mPlayerFactory.createPlayer(InCallTonePlayer.TONE_IN_CALL_QUALITY_NOTIFICATION)
+ mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_IN_CALL_QUALITY_NOTIFICATION)
.startTone();
}
call.displayDiagnosticMessage(messageId, message);
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
index 7e11b47..016b75e 100644
--- a/src/com/android/server/telecom/CallEndpointController.java
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -27,8 +27,11 @@
import android.telecom.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
@@ -49,6 +52,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 +61,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);
@@ -87,7 +91,7 @@
}
public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
- Log.d(this, "requestCallEndpointChange %s", endpoint);
+ Log.i(this, "requestCallEndpointChange %s", endpoint);
int route = mTypeToRouteMap.get(endpoint.getEndpointType());
String bluetoothAddress = getBluetoothAddress(endpoint);
@@ -99,7 +103,6 @@
}
if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) {
- Log.d(this, "requestCallEndpointChange: requested endpoint is already active");
callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
return;
}
@@ -130,21 +133,27 @@
return false;
}
CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState();
- // requested non-bt endpoint is already active
- if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH &&
- requestedRoute == currentAudioState.getRoute()) {
- return true;
- }
- // requested bt endpoint is already active
- if (requestedRoute == CallAudioState.ROUTE_BLUETOOTH &&
- currentAudioState.getActiveBluetoothDevice() != null &&
- requestedAddress.equals(
- currentAudioState.getActiveBluetoothDevice().getAddress())) {
- return true;
+ if (requestedRoute == currentAudioState.getRoute()) {
+ if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH) {
+ // The audio route (earpiece, speaker, etc.) is already active
+ // and Telecom can ignore the spam request!
+ Log.i(this, "iCERE: user requested a non-BT route that is already active");
+ return true;
+ } else if (hasSameBluetoothAddress(currentAudioState, requestedAddress)) {
+ // if the requested (BT route, device) is active, ignore the request...
+ Log.i(this, "iCERE: user requested a BT endpoint that is already active");
+ return true;
+ }
}
return false;
}
+ public boolean hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress) {
+ boolean hasActiveBtDevice = audioState.getActiveBluetoothDevice() != null;
+ return hasActiveBtDevice && requestedAddress.equals(
+ audioState.getActiveBluetoothDevice().getAddress());
+ }
+
private Bundle getErrorResult(int result) {
String message;
int resultCode;
@@ -190,45 +199,93 @@
}
mCallsManager.updateCallEndpoint(mActiveCallEndpoint);
- Set<Call> calls = mCallsManager.getTrackedCalls();
+ List<Call> calls = new ArrayList<>(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();
+ List<Call> calls = new ArrayList<>(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();
+ List<Call> calls = new ArrayList<>(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 7953324..c77b9ff 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -1,13 +1,19 @@
package com.android.server.telecom;
-import com.android.server.telecom.components.ErrorDialogActivity;
+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;
-import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.DefaultDialerManager;
@@ -19,6 +25,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;
@@ -32,7 +39,7 @@
public class CallIntentProcessor {
public interface Adapter {
void processOutgoingCallIntent(Context context, CallsManager callsManager,
- Intent intent, String callingPackage);
+ Intent intent, String callingPackage, FeatureFlags featureFlags);
void processIncomingCallIntent(CallsManager callsManager, Intent intent);
void processUnknownCallIntent(CallsManager callsManager, Intent intent);
}
@@ -45,9 +52,9 @@
@Override
public void processOutgoingCallIntent(Context context, CallsManager callsManager,
- Intent intent, String callingPackage) {
+ Intent intent, String callingPackage, FeatureFlags featureFlags) {
CallIntentProcessor.processOutgoingCallIntent(context, callsManager, intent,
- callingPackage, mDefaultDialerCache);
+ callingPackage, mDefaultDialerCache, featureFlags);
}
@Override
@@ -73,26 +80,26 @@
private final Context mContext;
private final CallsManager mCallsManager;
private final DefaultDialerCache mDefaultDialerCache;
+ private final FeatureFlags mFeatureFlags;
public CallIntentProcessor(Context context, CallsManager callsManager,
- DefaultDialerCache defaultDialerCache) {
+ DefaultDialerCache defaultDialerCache, FeatureFlags featureFlags) {
this.mContext = context;
this.mCallsManager = callsManager;
this.mDefaultDialerCache = defaultDialerCache;
+ this.mFeatureFlags = featureFlags;
}
public void processIntent(Intent intent, String callingPackage) {
final boolean isUnknownCall = intent.getBooleanExtra(KEY_IS_UNKNOWN_CALL, false);
Log.i(this, "onReceive - isUnknownCall: %s", isUnknownCall);
- Trace.beginSection("processNewCallCallIntent");
if (isUnknownCall) {
processUnknownCallIntent(mCallsManager, intent);
} else {
processOutgoingCallIntent(mContext, mCallsManager, intent, callingPackage,
- mDefaultDialerCache);
+ mDefaultDialerCache, mFeatureFlags);
}
- Trace.endSection();
}
@@ -107,7 +114,8 @@
CallsManager callsManager,
Intent intent,
String callingPackage,
- DefaultDialerCache defaultDialerCache) {
+ DefaultDialerCache defaultDialerCache,
+ FeatureFlags featureFlags) {
Uri handle = intent.getData();
String scheme = handle.getScheme();
@@ -164,13 +172,20 @@
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) {
- Toast.makeText(context, Looper.getMainLooper(),
- context.getString(R.string.toast_personal_call_msg),
- Toast.LENGTH_LONG).show();
+ if (featureFlags.telecomResolveHiddenDependencies()) {
+ 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),
+ Toast.LENGTH_LONG).show();
+ }
}
} else {
Log.i(CallIntentProcessor.class,
@@ -182,10 +197,21 @@
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());
+ isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags);
// If the broadcaster comes back with an immediate error, disconnect and show a dialog.
NewOutgoingCallIntentBroadcaster.CallDisposition disposition = broadcaster.evaluateCall();
@@ -218,16 +244,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"
@@ -298,4 +326,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 72aecac..4484e23 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -16,6 +16,7 @@
package com.android.server.telecom;
+import static android.provider.CallLog.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS;
import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;
@@ -24,12 +25,16 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.Country;
import android.location.CountryDetector;
import android.location.Location;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.UserHandle;
import android.os.PersistableBundle;
@@ -50,6 +55,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.Flags;
import java.util.Arrays;
import java.util.Locale;
@@ -75,18 +82,18 @@
private static class AddCallArgs {
public AddCallArgs(Context context, CallLog.AddCallParams params,
@Nullable LogCallCompletedListener logCallCompletedListener,
- @NonNull String callId) {
+ @NonNull Call call) {
this.context = context;
this.params = params;
this.logCallCompletedListener = logCallCompletedListener;
- this.callId = callId;
+ this.call = call;
}
// Since the members are accessed directly, we don't use the
// mXxxx notation.
public final Context context;
public final CallLog.AddCallParams params;
- public final String callId;
+ public final Call call;
@Nullable
public final LogCallCompletedListener logCallCompletedListener;
}
@@ -98,9 +105,9 @@
// a conference was merged successfully
private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY";
private static final UUID LOG_CALL_FAILED_ANOMALY_ID =
- UUID.fromString("1c4c15f3-ab4f-459c-b9ef-43d2988bae82");
+ UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702");
private static final String LOG_CALL_FAILED_ANOMALY_DESC =
- "Failed to record a call to the call log.";
+ "Based on the current user, Telecom detected failure to record a call to the call log.";
private final Context mContext;
private final CarrierConfigManager mCarrierConfigManager;
@@ -114,18 +121,24 @@
private static final String CALL_TYPE = "callType";
private static final String CALL_DURATION = "duration";
- private Object mLock;
+ private final Object mLock = new Object();
+ private Country mCurrentCountry;
private String mCurrentCountryIso;
+ private HandlerExecutor mCountryCodeExecutor;
+
+ private final FeatureFlags mFeatureFlags;
public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
- MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter) {
+ MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter,
+ FeatureFlags featureFlags) {
mContext = context;
mCarrierConfigManager = (CarrierConfigManager) mContext
.getSystemService(Context.CARRIER_CONFIG_SERVICE);
mPhoneAccountRegistrar = phoneAccountRegistrar;
mMissedCallNotifier = missedCallNotifier;
mAnomalyReporterAdapter = anomalyReporterAdapter;
- mLock = new Object();
+ mCountryCodeExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
+ mFeatureFlags = featureFlags;
}
@Override
@@ -164,9 +177,10 @@
* Call was NOT in the "choose account" phase when disconnected
* Call is NOT a conference call which had children (unless it was remotely hosted).
* Call is NOT a child call from a conference which was remotely hosted.
+ * Call has NOT indicated it should be skipped for logging in its extras
* Call is NOT simulating a single party conference.
* Call was NOT explicitly canceled, except for disconnecting from a conference.
- * Call is NOT an external call
+ * Call is NOT an external call or an external call on watch.
* Call is NOT disconnected because of merging into a conference.
* Call is NOT a self-managed call OR call is a self-managed call which has indicated it
* should be logged in its PhoneAccount
@@ -195,6 +209,11 @@
return false;
}
+ if (mFeatureFlags.telecomSkipLogBasedOnExtra() && call.getExtras() != null
+ && call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) {
+ return false;
+ }
+
// A child call of a conference which was remotely hosted; these didn't originate on this
// device and should not be logged.
if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
@@ -215,8 +234,10 @@
& Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
== Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE;
}
- // An external call
- if (call.isExternalCall()) {
+ // An external and non-watch call
+ if (call.isExternalCall() && (!mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_WATCH)
+ || !mFeatureFlags.telecomLogExternalWearableCalls())) {
return false;
}
@@ -233,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;
}
}
@@ -255,8 +275,13 @@
logCall(call, type, new LogCallCompletedListener() {
@Override
public void onLogCompleted(@Nullable Uri uri) {
- mMissedCallNotifier.showMissedCallNotification(
- new MissedCallNotifier.CallInfo(call));
+ if (mFeatureFlags.addCallUriForMissedCalls()){
+ mMissedCallNotifier.showMissedCallNotification(
+ new MissedCallNotifier.CallInfo(call), uri);
+ } else {
+ mMissedCallNotifier.showMissedCallNotification(
+ new MissedCallNotifier.CallInfo(call), /* uri= */ null);
+ }
}
}, result);
} else {
@@ -343,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 {
@@ -393,14 +418,32 @@
paramBuilder.setCallType(callLogType);
paramBuilder.setIsRead(call.isSelfManaged());
paramBuilder.setMissedReason(call.getMissedReason());
-
+ if (mFeatureFlags.businessCallComposer() && call.getExtras() != null) {
+ Bundle extras = call.getExtras();
+ boolean isBusinessCall =
+ extras.getBoolean(android.telecom.Call.EXTRA_IS_BUSINESS_CALL, false);
+ paramBuilder.setIsBusinessCall(isBusinessCall);
+ if (isBusinessCall) {
+ Log.i(TAG, "logging business call");
+ String assertedDisplayName =
+ extras.getString(android.telecom.Call.EXTRA_ASSERTED_DISPLAY_NAME, "");
+ if (assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) {
+ // avoid throwing an IllegalArgumentException and only log the first 256
+ // characters of the name.
+ paramBuilder.setAssertedDisplayName(
+ assertedDisplayName.substring(0, MAX_NUMBER_OF_CHARACTERS));
+ } else {
+ paramBuilder.setAssertedDisplayName(assertedDisplayName);
+ }
+ }
+ }
sendAddCallBroadcast(callLogType, call.getAgeMillis());
boolean okayToLog =
okayToLogCall(accountHandle, logNumber, call.isEmergencyCall());
if (okayToLog) {
AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(),
- logCallCompletedListener, call.getId());
+ logCallCompletedListener, call);
Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber)
+ ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres="
+ call.getHandlePresentation());
@@ -417,8 +460,8 @@
boolean okToLogEmergencyNumber = false;
CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
Context.CARRIER_CONFIG_SERVICE);
- PersistableBundle configBundle = configManager.getConfigForSubId(
- mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle));
+ PersistableBundle configBundle = (configManager != null) ? configManager.getConfigForSubId(
+ mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)) : null;
if (configBundle != null) {
okToLogEmergencyNumber = configBundle.getBoolean(
CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
@@ -531,19 +574,10 @@
AddCallArgs c = callList[i];
mListeners[i] = c.logCallCompletedListener;
try {
- // May block.
- ContentResolver resolver = c.context.getContentResolver();
- Pair<Integer, Integer> startStats = getCallLogStats(resolver);
- Log.i(TAG, "LogCall; about to log callId=%s, "
- + "startCount=%d, startMaxId=%d",
- c.callId, startStats.first, startStats.second);
-
result[i] = Calls.addCall(c.context, c.params);
- Pair<Integer, Integer> endStats = getCallLogStats(resolver);
- Log.i(TAG, "LogCall; logged callId=%s, uri=%s, "
- + "endCount=%d, endMaxId=%s",
- c.callId, 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
@@ -560,7 +594,7 @@
//
// We don't want to crash the whole process just because of that, so just log
// it instead.
- Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.callId);
+ Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId());
result[i] = null;
mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
LOG_CALL_FAILED_ANOMALY_DESC);
@@ -603,7 +637,7 @@
return Locale.getDefault().getCountry();
}
- return country.getCountryIso();
+ return country.getCountryCode();
}
/**
@@ -614,75 +648,33 @@
public String getCountryIso() {
synchronized (mLock) {
if (mCurrentCountryIso == null) {
- Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache...");
+ // Moving this into the constructor will pose issues if the service is not yet set
+ // up, causing a RemoteException to be thrown. Note that the callback is only
+ // registered if the country iso cache is null (so in an ideal setting, this should
+ // only require a one-time configuration).
final CountryDetector countryDetector =
(CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
- Country country = null;
if (countryDetector != null) {
- country = countryDetector.detectCountry();
-
- countryDetector.addCountryListener((newCountry) -> {
- Log.startSession("CLM.oCD");
- try {
- synchronized (mLock) {
- Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
- mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
- }
- } finally {
- Log.endSession();
- }
- }, Looper.getMainLooper());
+ countryDetector.registerCountryDetectorCallback(
+ mCountryCodeExecutor, this::countryCodeConsumer);
}
- mCurrentCountryIso = getCountryIsoFromCountry(country);
+ mCurrentCountryIso = getCountryIsoFromCountry(mCurrentCountry);
}
return mCurrentCountryIso;
}
}
-
- /**
- * Returns a pair containing the number of rows in the call log, as well as the maximum call log
- * ID. There is a limit of 500 entries in the call log for a phone account, so once we hit 500
- * we can reasonably expect that number to not change before and after logging a call.
- * We determine the maximum ID in the call log since this is a way we can objectively check if
- * the provider did record a call log entry or not. Ideally there should me more call log
- * entries after logging than before, and certainly not less.
- * @param resolver content resolver
- * @return pair with number of rows in the call log and max id.
- */
- private Pair<Integer, Integer> getCallLogStats(@NonNull ContentResolver resolver) {
+ /** Consumer to receive the country code if it changes. */
+ private void countryCodeConsumer(Country newCountry) {
+ Log.startSession("CLM.cCC");
try {
- final UserManager userManager = mContext.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;
+ Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
+ synchronized (mLock) {
+ mCurrentCountry = newCountry;
+ mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
}
- int maxCallId = -1;
- int numFound;
- Cursor countCursor = resolver.query(providerUri,
- new String[]{Calls._ID},
- null,
- null,
- Calls._ID + " DESC");
- try {
- numFound = countCursor.getCount();
- if (numFound > 0) {
- countCursor.moveToFirst();
- maxCallId = countCursor.getInt(0);
- }
- } finally {
- countCursor.close();
- }
- 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);
+ } finally {
+ Log.endSession();
}
}
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
new file mode 100644
index 0000000..6f16129
--- /dev/null
+++ b/src/com/android/server/telecom/CallSourceService.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;
+
+import android.os.Bundle;
+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);
+
+ void sendCallEvent(Call activeCall, String event, Bundle extras);
+}
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 1323633..d14a553 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -39,10 +39,10 @@
import android.telecom.Log;
import com.android.internal.telecom.ICallStreamingService;
-import com.android.server.telecom.voip.ParallelTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.voip.ParallelTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.util.ArrayList;
import java.util.List;
@@ -112,7 +112,7 @@
}
}
- public static class QueryCallStreamingTransaction extends VoipCallTransaction {
+ public static class QueryCallStreamingTransaction extends CallTransaction {
private final CallsManager mCallsManager;
public QueryCallStreamingTransaction(CallsManager callsManager) {
@@ -121,24 +121,24 @@
}
@Override
- public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletableFuture<CallTransactionResult> processTransaction(Void v) {
Log.i(this, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
if (mCallsManager.getCallStreamingController().isStreaming()) {
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_ALREADY_STREAMING"));
} else {
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
}
return future;
}
}
- public static class AudioInterceptionTransaction extends VoipCallTransaction {
+ public static class AudioInterceptionTransaction extends CallTransaction {
private Call mCall;
private boolean mEnterInterception;
@@ -150,16 +150,16 @@
}
@Override
- public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletableFuture<CallTransactionResult> processTransaction(Void v) {
Log.i(this, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
if (mEnterInterception) {
mCall.startStreaming();
} else {
mCall.stopStreaming();
}
- future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ future.complete(new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
null));
return future;
}
@@ -170,7 +170,7 @@
return new StreamingServiceTransaction(context, wrapper, call);
}
- public class StreamingServiceTransaction extends VoipCallTransaction {
+ public class StreamingServiceTransaction extends CallTransaction {
public static final String MESSAGE = "STREAMING_FAILED_NO_SENDER";
private final TransactionalServiceWrapper mWrapper;
private final Context mContext;
@@ -188,15 +188,16 @@
@SuppressLint("LongLogTag")
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.i(this, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
RoleManager roleManager = mContext.getSystemService(RoleManager.class);
PackageManager packageManager = mContext.getPackageManager();
if (roleManager == null || packageManager == null) {
Log.w(this, "processTransaction: Can't find system service");
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
@@ -204,8 +205,9 @@
RoleManager.ROLE_SYSTEM_CALL_STREAMING, mUserHandle);
if (holders.isEmpty()) {
Log.w(this, "processTransaction: Can't find streaming app");
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
@@ -215,8 +217,9 @@
PackageManager.GET_META_DATA, mUserHandle);
if (infos.isEmpty()) {
Log.w(this, "processTransaction: Can't find streaming service");
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
@@ -226,8 +229,9 @@
Manifest.permission.BIND_CALL_STREAMING_SERVICE)) {
Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
serviceInfo.packageName);
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ MESSAGE));
return future;
}
Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
@@ -238,8 +242,8 @@
| Context.BIND_FOREGROUND_SERVICE
| Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
Log.w(this, "Can't bind to streaming service");
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
"STREAMING_FAILED_SENDER_BINDING_ERROR"));
}
return future;
@@ -250,19 +254,19 @@
return new UnbindStreamingServiceTransaction();
}
- public class UnbindStreamingServiceTransaction extends VoipCallTransaction {
+ public class UnbindStreamingServiceTransaction extends CallTransaction {
public UnbindStreamingServiceTransaction() {
super(mTelecomLock);
}
@SuppressLint("LongLogTag")
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.i(this, "processTransaction (unbindStreaming");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
resetController();
- future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ future.complete(new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
null));
return future;
}
@@ -271,7 +275,7 @@
public class StartStreamingTransaction extends SerialTransaction {
private Call mCall;
- public StartStreamingTransaction(List<VoipCallTransaction> subTransactions, Call call,
+ public StartStreamingTransaction(List<CallTransaction> subTransactions, Call call,
TelecomSystem.SyncRoot lock) {
super(subTransactions, lock);
mCall = call;
@@ -283,7 +287,7 @@
}
}
- public VoipCallTransaction getStartStreamingTransaction(CallsManager callsManager,
+ public CallTransaction getStartStreamingTransaction(CallsManager callsManager,
TransactionalServiceWrapper wrapper, Call call, TelecomSystem.SyncRoot lock) {
// start streaming transaction flow:
// 1. make sure there's no ongoing streaming call --> bind to EXO
@@ -292,7 +296,7 @@
// If bind to EXO failed, add transaction for stop the streaming
// create list for multiple transactions
- List<VoipCallTransaction> transactions = new ArrayList<>();
+ List<CallTransaction> transactions = new ArrayList<>();
transactions.add(new QueryCallStreamingTransaction(callsManager));
transactions.add(new AudioInterceptionTransaction(call, true, lock));
transactions.add(getCallStreamingServiceTransaction(
@@ -300,10 +304,10 @@
return new StartStreamingTransaction(transactions, call, lock);
}
- public VoipCallTransaction getStopStreamingTransaction(Call call, TelecomSystem.SyncRoot lock) {
+ public CallTransaction getStopStreamingTransaction(Call call, TelecomSystem.SyncRoot lock) {
// TODO: implement this
// Stop streaming transaction flow:
- List<VoipCallTransaction> transactions = new ArrayList<>();
+ List<CallTransaction> transactions = new ArrayList<>();
// 1. unbind to call streaming service
transactions.add(getUnbindStreamingServiceTransaction());
@@ -348,7 +352,7 @@
mTransactionalServiceWrapper.getTransactionManager().addTransaction(transaction,
new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
+ public void onResult(CallTransactionResult result) {
// ignore
}
@@ -362,7 +366,7 @@
}
}
- private class CallStreamingStateChangeTransaction extends VoipCallTransaction {
+ private class CallStreamingStateChangeTransaction extends CallTransaction {
@StreamingCall.StreamingCallState int mState;
public CallStreamingStateChangeTransaction(@StreamingCall.StreamingCallState int state) {
@@ -371,15 +375,16 @@
}
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
try {
mService.onCallStreamingStateChanged(mState);
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
} catch (RemoteException e) {
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, "Exception when request "
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+ "Exception when request "
+ "setting state to streaming app."));
}
return future;
@@ -390,10 +395,10 @@
ServiceConnection {
private Call mCall;
private TransactionalServiceWrapper mWrapper;
- private CompletableFuture<VoipCallTransactionResult> mFuture;
+ private CompletableFuture<CallTransactionResult> mFuture;
public CallStreamingServiceConnection(Call call, TransactionalServiceWrapper wrapper,
- CompletableFuture<VoipCallTransactionResult> future) {
+ CompletableFuture<CallTransactionResult> future) {
mCall = call;
mWrapper = wrapper;
mFuture = future;
@@ -404,12 +409,12 @@
try {
Log.i(this, "onServiceConnected: " + name);
onConnectedInternal(mCall, mWrapper, service);
- mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ mFuture.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
} catch (RemoteException e) {
resetController();
- mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ mFuture.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
StreamingServiceTransaction.MESSAGE));
}
}
@@ -432,8 +437,8 @@
private void clearBinding() {
resetController();
if (!mFuture.isDone()) {
- mFuture.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
+ mFuture.complete(new CallTransactionResult(
+ 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 98e67bb..9670d6a 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -73,15 +73,14 @@
import android.os.Process;
import android.os.ResultReceiver;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.SystemVibrator;
-import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.BlockedNumberContract;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumbersManager;
import android.provider.CallLog.Calls;
import android.provider.Settings;
-import android.sysprop.TelephonyProperties;
import android.telecom.CallAttributes;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
@@ -117,6 +116,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IntentForwarderActivity;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
@@ -129,10 +129,15 @@
import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
import com.android.server.telecom.callfiltering.DndCallFilter;
import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
import com.android.server.telecom.callredirection.CallRedirectionProcessor;
+import com.android.server.telecom.callsequencing.CallSequencingController;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
-import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
@@ -141,8 +146,8 @@
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallMonitor;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
+import com.android.server.telecom.callsequencing.TransactionManager;
import java.util.ArrayList;
import java.util.Arrays;
@@ -211,7 +216,7 @@
void onHoldToneRequested(Call call);
void onExternalCallChanged(Call call, boolean isExternalCall);
void onCallStreamingStateChanged(Call call, boolean isStreaming);
- void onDisconnectedTonePlaying(boolean isTonePlaying);
+ void onDisconnectedTonePlaying(Call call, boolean isTonePlaying);
void onConnectionTimeChanged(Call call);
void onConferenceStateChanged(Call call, boolean isConference);
void onCdmaConferenceSwap(Call call);
@@ -223,6 +228,29 @@
void performAction();
}
+ /**
+ * @hide
+ */
+ public interface Response<IN, OUT> {
+
+ /**
+ * Provide a set of results.
+ *
+ * @param request The original request.
+ * @param result The results.
+ */
+ void onResult(IN request, OUT... result);
+
+ /**
+ * Indicates the inability to provide results.
+ *
+ * @param request The original request.
+ * @param code An integer code indicating the reason for failure.
+ * @param msg A message explaining the reason for failure.
+ */
+ void onError(IN request, int code, String msg);
+ }
+
private static final String TAG = "CallsManager";
/**
@@ -287,15 +315,14 @@
public static final String EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG =
"Exception thrown while retrieving list of potential phone accounts when placing an "
+ "emergency call.";
- public static final UUID EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID =
- UUID.fromString("f9a916c8-8d61-4550-9ad3-11c2e84f6364");
- public static final String EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG =
- "An emergency call was disconnected after the connection was created but before the "
- + "call was successfully added to CallsManager.";
public static final UUID EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID =
UUID.fromString("2e994acb-1997-4345-8bf3-bad04303de26");
public static final String EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG =
"An emergency call was aborted since there were no available phone accounts.";
+ public static final UUID TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID =
+ UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
+ public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
+ "Telephony has a default MO acct but Telecom prompted user for MO";
private static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -420,6 +447,7 @@
private final InCallController mInCallController;
private final CallDiagnosticServiceController mCallDiagnosticServiceController;
private final CallAudioManager mCallAudioManager;
+ /** @deprecated not used any more */
private final CallRecordingTonePlayer mCallRecordingTonePlayer;
private RespondViaSmsManager mRespondViaSmsManager;
private final Ringer mRinger;
@@ -464,14 +492,24 @@
private final TransactionManager mTransactionManager;
private final UserManager mUserManager;
private final CallStreamingNotification mCallStreamingNotification;
+ private final BlockedNumbersManager mBlockedNumbersManager;
+ private final CallsManagerCallSequencingAdapter mCallSequencingAdapter;
+ private final FeatureFlags mFeatureFlags;
+ private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+
+ private final IncomingCallFilterGraphProvider mIncomingCallFilterGraphProvider;
private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
new ConnectionServiceFocusManager.CallsManagerRequester() {
@Override
public void releaseConnectionService(
ConnectionServiceFocusManager.ConnectionServiceFocus connectionService) {
+ if (connectionService == null) {
+ Log.i(this, "releaseConnectionService: connectionService is null");
+ return;
+ }
mCalls.stream()
- .filter(c -> c.getConnectionServiceWrapper().equals(connectionService))
+ .filter(c -> connectionService.equals(c.getConnectionServiceWrapper()))
.forEach(c -> c.disconnect("release " +
connectionService.getComponentName().getPackageName()));
}
@@ -496,6 +534,8 @@
private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
private final MmiUtils mMmiUtils = new MmiUtils();
+
+ private TelecomMetricsController mMetricsController;
/**
* Listener to PhoneAccountRegistrar events.
*/
@@ -528,7 +568,8 @@
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
- || SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
+ || BlockedNumbersManager
+ .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
updateEmergencyCallNotificationAsync(context);
}
}
@@ -577,7 +618,13 @@
BlockedNumbersAdapter blockedNumbersAdapter,
TransactionManager transactionManager,
EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
- CallStreamingNotification callStreamingNotification) {
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ CallStreamingNotification callStreamingNotification,
+ BluetoothDeviceManager bluetoothDeviceManager,
+ FeatureFlags featureFlags,
+ com.android.internal.telephony.flags.FeatureFlags telephonyFlags,
+ IncomingCallFilterGraphProvider incomingCallFilterGraphProvider,
+ TelecomMetricsController metricsController) {
mContext = context;
mLock = lock;
@@ -596,25 +643,39 @@
mEmergencyCallHelper = emergencyCallHelper;
mCallerInfoLookupHelper = callerInfoLookupHelper;
mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
+ mIncomingCallFilterGraphProvider = incomingCallFilterGraphProvider;
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
- CallAudioRouteStateMachine callAudioRouteStateMachine =
- callAudioRouteStateMachineFactory.create(
- context,
- this,
- bluetoothManager,
- wiredHeadsetManager,
- statusBarNotifier,
- audioServiceFactory,
- CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
- asyncCallAudioTaskExecutor
- );
- callAudioRouteStateMachine.initialize();
+ CallAudioRouteAdapter callAudioRouteAdapter;
+ // TODO: add another flag check when
+ // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
+ // available and return true
+ if (!featureFlags.useRefactoredAudioRouteSwitching()) {
+ callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
+ context,
+ this,
+ bluetoothManager,
+ wiredHeadsetManager,
+ statusBarNotifier,
+ audioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+ asyncCallAudioTaskExecutor,
+ communicationDeviceTracker,
+ featureFlags
+ );
+ } else {
+ callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
+ new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
+ statusBarNotifier, featureFlags, metricsController);
+ }
+ callAudioRouteAdapter.initialize();
+ bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+ bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
new CallAudioRoutePeripheralAdapter(
- callAudioRouteStateMachine,
+ callAudioRouteAdapter,
bluetoothManager,
wiredHeadsetManager,
mDockManager,
@@ -627,10 +688,10 @@
audioManager.generateAudioSessionId()));
InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(
callAudioRoutePeripheralAdapter, lock, toneGeneratorFactory, mediaPlayerFactory,
- () -> audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0);
+ () -> audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0, featureFlags);
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,
@@ -642,14 +703,20 @@
ringtoneFactory, systemVibrator,
new Ringer.VibrationEffectProxy(), mInCallController,
mContext.getSystemService(NotificationManager.class),
- accessibilityManagerAdapter);
- mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
- mTimeoutsAdapter, mLock);
- mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
+ accessibilityManagerAdapter, featureFlags);
+ if (featureFlags.telecomResolveHiddenDependencies()) {
+ // This is now deprecated
+ mCallRecordingTonePlayer = null;
+ } else {
+ mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
+ mTimeoutsAdapter, mLock);
+ }
+ mCallAudioManager = new CallAudioManager(callAudioRouteAdapter,
this, callAudioModeStateMachineFactory.create(systemStateHelper,
- (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)),
+ (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE),
+ featureFlags, communicationDeviceTracker),
playerFactory, mRinger, new RingbackPlayer(playerFactory),
- bluetoothStateReceiver, mDtmfLocalTonePlayer);
+ bluetoothStateReceiver, mDtmfLocalTonePlayer, featureFlags);
mConnectionSvrFocusMgr = connectionServiceFocusManagerFactory.create(mRequester);
mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
@@ -657,27 +724,44 @@
mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier,
- mAnomalyReporter);
+ mAnomalyReporter, featureFlags);
mConnectionServiceRepository =
- new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
+ new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this,
+ featureFlags);
mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
mClockProxy = clockProxy;
mToastFactory = toastFactory;
mRoleManagerAdapter = roleManagerAdapter;
+ mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
mTransactionManager = transactionManager;
mBlockedNumbersAdapter = blockedNumbersAdapter;
mCallStreamingController = new CallStreamingController(mContext, mLock);
- mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
mCallStreamingNotification = callStreamingNotification;
+ mFeatureFlags = featureFlags;
+ mTelephonyFeatureFlags = telephonyFlags;
+ mMetricsController = metricsController;
+ mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+ ? mContext.getSystemService(BlockedNumbersManager.class)
+ : null;
+ mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this,
+ new CallSequencingController(this, mFeatureFlags.enableCallSequencing()),
+ mFeatureFlags.enableCallSequencing());
+ if (mFeatureFlags.useImprovedListenerOrder()) {
+ mListeners.add(mInCallController);
+ }
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
mListeners.add(mCallLogManager);
- mListeners.add(mInCallController);
+ if (!mFeatureFlags.useImprovedListenerOrder()) {
+ mListeners.add(mInCallController);
+ }
mListeners.add(mCallEndpointController);
mListeners.add(mCallDiagnosticServiceController);
mListeners.add(mCallAudioManager);
- mListeners.add(mCallRecordingTonePlayer);
+ if (!featureFlags.telecomResolveHiddenDependencies()) {
+ mListeners.add(mCallRecordingTonePlayer);
+ }
mListeners.add(missedCallNotifier);
mListeners.add(mDisconnectedCallNotifier);
mListeners.add(mHeadsetMediaButton);
@@ -695,7 +779,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());
@@ -704,7 +788,7 @@
IntentFilter intentFilter = new IntentFilter(
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
- intentFilter.addAction(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+ intentFilter.addAction(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
mGraphHandlerThreads = new LinkedList<>();
@@ -748,11 +832,14 @@
@Override
@VisibleForTesting
public void onSuccessfulOutgoingCall(Call call, int callState) {
- Log.v(this, "onSuccessfulOutgoingCall, %s", call);
+ Log.v(this, "onSuccessfulOutgoingCall, call=[%s], state=[%d]", call, callState);
call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp(
call.getAssociatedUser()));
- setCallState(call, callState, "successful outgoing call");
+ if (!mFeatureFlags.fixAudioFlickerForOutgoingCalls()) {
+ setCallState(call, callState, "successful outgoing call");
+ }
+
if (!mCalls.contains(call)) {
// Call was not added previously in startOutgoingCall due to it being a potential MMI
// code, so add it now.
@@ -764,7 +851,18 @@
listener.onConnectionServiceChanged(call, null, call.getConnectionService());
}
- markCallAsDialing(call);
+ if (mFeatureFlags.fixAudioFlickerForOutgoingCalls()) {
+ // Allow the ConnectionService to start the call in the active state. This case is
+ // helpful for conference calls or meetings that can skip the dialing stage.
+ if (callState == CallState.ACTIVE) {
+ setCallState(call, callState, "skipping the dialing state and setting active");
+ } else {
+ markCallAsDialing(call);
+ }
+ }
+ else{
+ markCallAsDialing(call);
+ }
}
@Override
@@ -783,18 +881,25 @@
? 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() ||
- extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+ (!performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING))) {
Log.i(this, "Skipping call filtering for %s (ecm=%b, "
+ "networkIdentifiedEmergencyCall = %b, emergencySmsMode = %b, "
+ "selfMgd=%b, skipExtra=%b)",
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()
@@ -805,30 +910,45 @@
.build(), false);
incomingCall.setIsUsingCallFiltering(false);
return;
+ } else if (performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+ IncomingCallFilterGraph graph = setupDndFilterOnlyGraph(incomingCall);
+ graph.performFiltering();
+ return;
}
IncomingCallFilterGraph graph = setUpCallFilterGraph(incomingCall);
graph.performFiltering();
}
+ private IncomingCallFilterGraph setupDndFilterOnlyGraph(Call incomingHfpCall) {
+ incomingHfpCall.setIsUsingCallFiltering(true);
+ DndCallFilter dndCallFilter = new DndCallFilter(incomingHfpCall, mRinger);
+ IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(
+ incomingHfpCall,
+ this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mFeatureFlags, mLock);
+ graph.addFilter(dndCallFilter);
+ mGraphHandlerThreads.add(graph.getHandlerThread());
+ return graph;
+ }
+
private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) {
+ TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
incomingCall.setIsUsingCallFiltering(true);
String carrierPackageName = getCarrierPackageName();
UserHandle userHandle = incomingCall.getAssociatedUser();
- String defaultDialerPackageName = TelecomManager.from(mContext).
- getDefaultDialerPackage(userHandle);
+ String defaultDialerPackageName = telecomManager.getDefaultDialerPackage(userHandle);
String userChosenPackageName = getRoleManagerAdapter().
getDefaultCallScreeningApp(userHandle);
- AppLabelProxy appLabelProxy = packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName);
+ AppLabelProxy appLabelProxy = (packageName, user) -> AppLabelProxy.Util.getAppLabel(
+ mContext, user, packageName, mFeatureFlags);
ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
- IncomingCallFilterGraph graph = new IncomingCallFilterGraph(incomingCall,
- this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
+ IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall,
+ this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mFeatureFlags, mLock);
DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
mCallerInfoLookupHelper);
BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
- mCallerInfoLookupHelper, new BlockCheckerAdapter());
+ mCallerInfoLookupHelper, new BlockCheckerAdapter(mFeatureFlags), mFeatureFlags);
DndCallFilter dndCallFilter = new DndCallFilter(incomingCall, getRinger());
CallScreeningServiceFilter carrierCallScreeningServiceFilter =
new CallScreeningServiceFilter(incomingCall, carrierPackageName,
@@ -863,6 +983,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
@@ -888,8 +1009,10 @@
if (incomingCall.getState() != CallState.DISCONNECTED &&
incomingCall.getState() != CallState.DISCONNECTING) {
- setCallState(incomingCall, CallState.RINGING,
- result.shouldAllowCall ? "successful incoming call" : "blocking call");
+ if (!mFeatureFlags.separatelyBindToBtIncallService()) {
+ setCallState(incomingCall, CallState.RINGING,
+ result.shouldAllowCall ? "successful incoming call" : "blocking call");
+ }
} else {
Log.i(this, "onCallFilteringCompleted: call already disconnected.");
return;
@@ -934,6 +1057,11 @@
}
if (result.shouldAllowCall) {
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ mInCallController.bindToBTService(incomingCall, null);
+ incomingCall.setBtIcsFuture(mInCallController.getBtBindingFuture(incomingCall));
+ setCallState(incomingCall, CallState.RINGING, "successful incoming call");
+ }
incomingCall.setPostCallPackageName(
getRoleManagerAdapter().getDefaultCallScreeningApp(
incomingCall.getAssociatedUser()
@@ -948,7 +1076,6 @@
"Exceeds maximum number of ringing calls.");
incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
autoMissCallAndLog(incomingCall, result);
- return;
}
} else if (hasMaximumManagedDialingCalls(incomingCall)) {
if (shouldSilenceInsteadOfReject(incomingCall)) {
@@ -958,7 +1085,6 @@
"dialing calls.");
incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_DIALING);
autoMissCallAndLog(incomingCall, result);
- return;
}
} else if (result.shouldScreenViaAudio) {
Log.i(this, "onCallFilteringCompleted: starting background audio processing");
@@ -977,6 +1103,9 @@
} else {
if (result.shouldReject) {
Log.i(this, "onCallFilteringCompleted: blocked call, rejecting.");
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ setCallState(incomingCall, CallState.RINGING, "blocking call");
+ }
incomingCall.reject(false, null);
}
if (result.shouldAddToCallLog) {
@@ -990,7 +1119,7 @@
if (result.shouldShowNotification) {
Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
mMissedCallNotifier.showMissedCallNotification(
- new MissedCallNotifier.CallInfo(incomingCall));
+ new MissedCallNotifier.CallInfo(incomingCall), /* uri= */ null);
}
}
}
@@ -1248,9 +1377,7 @@
@Override
public void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
Bundle extras, boolean isLegacy) {
- if (isLegacy) {
- requestHandoverViaEvents(call, handoverTo, videoState, extras);
- } else {
+ if (!isLegacy) {
requestHandover(call, handoverTo, videoState, extras);
}
}
@@ -1303,7 +1430,7 @@
return mCallAudioManager;
}
- InCallController getInCallController() {
+ public InCallController getInCallController() {
return mInCallController;
}
@@ -1311,7 +1438,7 @@
return mCallEndpointController;
}
- EmergencyCallHelper getEmergencyCallHelper() {
+ public EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
@@ -1432,7 +1559,8 @@
false /* forceAttachToExistingConnection */,
isConference, /* isConference */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
// Ensure new calls related to self-managed calls/connections are set as such. This will
// be overridden when the actual connection is returned in startCreateConnection, however
// doing this now ensures the logs and any other logic will treat this call as self-managed
@@ -1447,9 +1575,7 @@
if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
call.setIsTransactionalCall(true);
call.setCallingPackageIdentity(extras);
- call.setConnectionCapabilities(
- extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
- CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ call.setTransactionalCapabilities(extras);
call.setTargetPhoneAccount(phoneAccountHandle);
if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
CharSequence displayName = extras.getCharSequence(CallAttributes.DISPLAY_NAME_KEY);
@@ -1459,7 +1585,10 @@
}
}
// Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above.
- call.setAssociatedUser(phoneAccountHandle.getUserHandle());
+ UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+ mFeatureFlags.associatedUserRefactorForWorkProfile(),
+ getPhoneAccountRegistrar(), getCurrentUserHandle(), phoneAccountHandle);
+ call.setAssociatedUser(associatedUser);
}
if (phoneAccount != null) {
@@ -1579,15 +1708,25 @@
// Check if the target phone account is possibly in ECBM.
call.setIsInECBM(getEmergencyCallHelper()
.isLastOutgoingEmergencyCallPAH(call.getTargetPhoneAccount()));
- // If the phone account user profile is paused or the call isn't visible to the secondary/
- // guest user, reject the non-emergency incoming call. When the current user is the admin,
- // we need to allow the calls to go through if the work profile isn't paused. We should
- // always allow emergency calls and also allow non-emergency calls when ECBM is active for
- // the phone account.
- if ((mUserManager.isQuietModeEnabled(call.getAssociatedUser())
- || (!mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier())
- && !isCallVisibleForUser(call, mCurrentUserHandle)))
- && !call.isEmergencyCall() && !call.isInECBM()) {
+
+ // Check if call is visible to the current user.
+ boolean isCallHiddenFromProfile = !isCallVisibleForUser(call, mCurrentUserHandle);
+ // For admins, we should check if the work profile is paused in order to reject
+ // the call.
+ 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
+ // is active for the phone account.
+ if (isCallHiddenFromProfile && !call.isEmergencyCall() && !call.isInECBM()) {
Log.d(TAG, "Rejecting non-emergency call because the owner %s is not running.",
phoneAccountHandle.getUserHandle());
call.setMissedReason(USER_MISSED_NOT_RUNNING);
@@ -1660,11 +1799,15 @@
true /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
call.initAnalytics();
// For unknown calls, base the associated user off of the target phone account handle.
- call.setAssociatedUser(phoneAccountHandle.getUserHandle());
+ UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+ mFeatureFlags.associatedUserRefactorForWorkProfile(),
+ getPhoneAccountRegistrar(), getCurrentUserHandle(), phoneAccountHandle);
+ call.setAssociatedUser(associatedUser);
setIntentExtrasAndStartTime(call, extras);
call.addListener(this);
notifyStartCreateConnection(call);
@@ -1778,14 +1921,13 @@
false /* forceAttachToExistingConnection */,
isConference, /* isConference */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
call.setIsTransactionalCall(true);
call.setCallingPackageIdentity(extras);
- call.setConnectionCapabilities(
- extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
- CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ call.setTransactionalCapabilities(extras);
if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
CharSequence displayName = extras.getCharSequence(
CallAttributes.DISPLAY_NAME_KEY);
@@ -1800,19 +1942,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
@@ -1891,10 +2039,18 @@
if (exception != null){
Log.e(TAG, exception, "Error retrieving list of potential phone accounts.");
if (finalCall.isEmergencyCall()) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_RETRIEVING_ACCOUNT_EMERGENCY);
+ }
mAnomalyReporter.reportAnomaly(
EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_UUID,
EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG);
} else {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_RETRIEVING_ACCOUNT);
+ }
mAnomalyReporter.reportAnomaly(
EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_UUID,
EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_MSG);
@@ -1926,27 +2082,25 @@
return CompletableFuture.completedFuture(
Collections.singletonList(suggestion));
}
- return PhoneAccountSuggestionHelper.bindAndGetSuggestions(mContext,
+ Context userContext = mContext.createContextAsUser(getCurrentUserHandle(), 0);
+ return PhoneAccountSuggestionHelper.bindAndGetSuggestions(userContext,
finalCall.getHandle(), potentialPhoneAccounts);
}, new LoggedHandlerExecutor(outgoingCallHandler, "CM.cOCSS", mLock));
// This future checks the status of existing calls and attempts to make room for the
- // outgoing call. The future returned by the inner method will usually be pre-completed --
- // we only pause here if user interaction is required to disconnect a self-managed call.
- // It runs after the account handle is set, independently of the phone account suggestion
- // future.
- CompletableFuture<Call> makeRoomForCall = setAccountHandle.thenComposeAsync(
+ // outgoing call.
+ CompletableFuture<Boolean> makeRoomForCall = setAccountHandle.thenComposeAsync(
potentialPhoneAccounts -> {
Log.i(CallsManager.this, "make room for outgoing call stage");
if (mMmiUtils.isPotentialInCallMMICode(handle) && !isSelfManaged) {
- return CompletableFuture.completedFuture(finalCall);
+ return CompletableFuture.completedFuture(true);
}
// If a call is being reused, then it has already passed the
// makeRoomForOutgoingCall check once and will fail the second time due to the
// call transitioning into the CONNECTING state.
if (isReusedCall) {
- return CompletableFuture.completedFuture(finalCall);
+ return CompletableFuture.completedFuture(true);
} else {
Call reusableCall = reuseOutgoingCall(handle);
if (reusableCall != null) {
@@ -1973,48 +2127,75 @@
finalCall.getTargetPhoneAccount(), finalCall);
}
finalCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
- return CompletableFuture.completedFuture(null);
+ return CompletableFuture.completedFuture(false);
}
- // If we can not supportany more active calls, our options are to move a call
+ // If we can not support any more active calls, our options are to move a call
// to hold, disconnect a call, or cancel this call altogether.
- boolean isRoomForCall = finalCall.isEmergencyCall() ?
- makeRoomForOutgoingEmergencyCall(finalCall) :
- makeRoomForOutgoingCall(finalCall);
- if (!isRoomForCall) {
- Call foregroundCall = getForegroundCall();
- Log.d(CallsManager.this, "No more room for outgoing call %s ", finalCall);
- if (foregroundCall.isSelfManaged()) {
- // If the ongoing call is a self-managed call, then prompt the user to
- // ask if they'd like to disconnect their ongoing call and place the
- // outgoing call.
- Log.i(CallsManager.this, "Prompting user to disconnect "
- + "self-managed call");
- finalCall.setOriginalCallIntent(originalIntent);
- CompletableFuture<Call> completionFuture = new CompletableFuture<>();
- startCallConfirmation(finalCall, completionFuture);
- return completionFuture;
- } else {
- // If the ongoing call is a managed call, we will prevent the outgoing
- // call from dialing.
- if (isConference) {
- notifyCreateConferenceFailed(finalCall.getTargetPhoneAccount(),
- finalCall);
- } else {
- notifyCreateConnectionFailed(
- finalCall.getTargetPhoneAccount(), finalCall);
- }
+ CompletableFuture<Boolean> isRoomForCallFuture =
+ mCallSequencingAdapter.makeRoomForOutgoingCall(
+ finalCall.isEmergencyCall(), finalCall);
+ isRoomForCallFuture.exceptionally((throwable -> {
+ if (throwable != null) {
+ Log.w(CallsManager.this,
+ "Exception thrown in makeRoomForOutgoing*Call, "
+ + "returning false. Ex:" + throwable);
}
- Log.i(CallsManager.this, "Aborting call since there's no room");
+ return false;
+ }));
+ return isRoomForCallFuture;
+ }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSMCP", mLock));
+
+ // The future returned by the inner method will usually be pre-completed --
+ // we only pause here if user interaction is required to disconnect a self-managed call.
+ // It runs after the account handle is set, independently of the phone account suggestion
+ // future.
+ CompletableFuture<Call> makeRoomResultHandler = makeRoomForCall
+ .thenComposeAsync((isRoom) -> {
+ // If we have an ongoing emergency call, we would have already notified
+ // connection failure for the new call being placed. Catch this so we don't
+ // resend it again.
+ boolean hasOngoingEmergencyCall = !finalCall.isEmergencyCall()
+ && isInEmergencyCall();
+ if (isRoom) {
+ return CompletableFuture.completedFuture(finalCall);
+ } else if (hasOngoingEmergencyCall) {
return CompletableFuture.completedFuture(null);
}
- return CompletableFuture.completedFuture(finalCall);
- }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSMCP", mLock));
+ Call foregroundCall = getForegroundCall();
+ Log.d(CallsManager.this, "No more room for outgoing call %s ",
+ finalCall);
+ if (foregroundCall.isSelfManaged()) {
+ // If the ongoing call is a self-managed call, then prompt the
+ // user to ask if they'd like to disconnect their ongoing call
+ // and place the outgoing call.
+ Log.i(CallsManager.this, "Prompting user to disconnect "
+ + "self-managed call");
+ finalCall.setOriginalCallIntent(originalIntent);
+ CompletableFuture<Call> completionFuture =
+ new CompletableFuture<>();
+ startCallConfirmation(finalCall, completionFuture);
+ return completionFuture;
+ } else {
+ // If the ongoing call is a managed call, we will prevent the
+ // outgoing call from dialing.
+ if (isConference) {
+ notifyCreateConferenceFailed(
+ finalCall.getTargetPhoneAccount(),
+ finalCall);
+ } else {
+ notifyCreateConnectionFailed(
+ finalCall.getTargetPhoneAccount(), finalCall);
+ }
+ }
+ Log.i(CallsManager.this, "Aborting call since there's no room");
+ return CompletableFuture.completedFuture(null);
+ }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.mROC", mLock));
// The outgoing call can be placed, go forward. This future glues together the results of
// the account suggestion stage and the make room for call stage.
CompletableFuture<Pair<Call, List<PhoneAccountSuggestion>>> preSelectStage =
- makeRoomForCall.thenCombine(suggestionFuture, Pair::create);
+ makeRoomResultHandler.thenCombine(suggestionFuture, Pair::create);
mLatestPreAccountSelectionFuture = preSelectStage;
// This future takes the list of suggested accounts and the call and determines if more
@@ -2034,7 +2215,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(
@@ -2054,7 +2235,12 @@
+ " available accounts.");
showErrorMessage(R.string.cant_call_due_to_no_supported_service);
mListeners.forEach(l -> l.onCreateConnectionFailed(callToPlace));
- if (callToPlace.isEmergencyCall()){
+ if (callToPlace.isEmergencyCall()) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(
+ ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT);
+ }
mAnomalyReporter.reportAnomaly(
EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID,
EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG);
@@ -2067,6 +2253,31 @@
return CompletableFuture.completedFuture(Pair.create(callToPlace,
accountSuggestions.get(0).getPhoneAccountHandle()));
}
+
+ // At this point Telecom is requesting the user to select a phone
+ // account. However, Telephony is reporting that the user has a default
+ // outgoing account (which is denoted by a non-negative subId number).
+ // At some point, Telecom and Telephony are out of sync with the default
+ // outgoing calling account.
+ if(mFeatureFlags.telephonyHasDefaultButTelecomDoesNot()) {
+ // 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) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(
+ ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_DEFAULT_MO_ACCOUNT_MISMATCH);
+ }
+ mAnomalyReporter.reportAnomaly(
+ TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID,
+ TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG);
+ }
+ }
+ }
+
// This is the state where the user is expected to select an account
callToPlace.setState(CallState.SELECT_PHONE_ACCOUNT,
"needs account selection");
@@ -2194,15 +2405,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;
@@ -2315,8 +2546,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,
@@ -2361,8 +2592,8 @@
theCall,
new AppLabelProxy() {
@Override
- public CharSequence getAppLabel(String packageName) {
- return Util.getAppLabel(mContext.getPackageManager(), packageName);
+ public CharSequence getAppLabel(String packageName, UserHandle userHandle) {
+ return Util.getAppLabel(mContext, userHandle, packageName, mFeatureFlags);
}
}).process();
future.thenApply( v -> {
@@ -2527,6 +2758,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) {
@@ -2815,9 +3049,13 @@
}
if (call.isEmergencyCall()) {
- Executors.defaultThreadFactory().newThread(() ->
- BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext))
- .start();
+ Executors.defaultThreadFactory().newThread(() -> {
+ if (mBlockedNumbersManager != null) {
+ mBlockedNumbersManager.notifyEmergencyContact();
+ } else {
+ BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext);
+ }
+ }).start();
}
final boolean requireCallCapableAccountByHandle = mContext.getResources().getBoolean(
@@ -2847,6 +3085,10 @@
// If an exceptions is thrown while creating the connection, prompt the user to
// generate a bugreport and force disconnect.
Log.e(TAG, exception, "Exception thrown while establishing connection.");
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_ESTABLISHING_CONNECTION);
+ }
mAnomalyReporter.reportAnomaly(
EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_UUID,
EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_MSG);
@@ -2890,14 +3132,31 @@
public void answerCall(Call call, int videoState) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to answer a non-existent call %s", call);
- } else if (call.isTransactionalCall()) {
+ }
+ mCallSequencingAdapter.answerCall(call, videoState);
+ }
+
+ /**
+ * CS: Hold any existing calls, request focus, and then set the call state to answered state.
+ * <p>
+ * T: Call TransactionalServiceWrapper, which then generates transactions to hold calls
+ * {@link #transactionHoldPotentialActiveCallForNewCall} and then move the active call focus
+ * {@link #requestNewCallFocusAndVerify} and notify the remote VOIP app of the call state
+ * moving to active.
+ * <p>
+ * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
+ */
+ public void answerCallOld(Call call, int videoState) {
+ if (call.isTransactionalCall()) {
// InCallAdapter is requesting to answer the given transactioanl call. Must get an ack
// from the client via a transaction before answering.
call.answer(videoState);
} else {
+ if (!mFeatureFlags.genAnomReportOnFocusTimeout()) {
+ Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+ Log.d(this, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall);
+ }
// Hold or disconnect the active call and request call focus for the incoming call.
- Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- Log.d(this, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall);
holdActiveCallForNewCall(call);
mConnectionSvrFocusMgr.requestFocus(
call,
@@ -2955,7 +3214,7 @@
}
CharSequence requestingAppName = AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), requestingPackageName);
+ mContext, call.getAssociatedUser(), requestingPackageName, mFeatureFlags);
if (requestingAppName == null) {
requestingAppName = requestingPackageName;
}
@@ -3049,9 +3308,8 @@
* @return {@code true} if the speakerphone should automatically be enabled.
*/
private static boolean isSpeakerEnabledForVideoCalls() {
- return TelephonyProperties.videocall_audio_output()
- .orElse(TelecomManager.AUDIO_OUTPUT_DEFAULT)
- == TelecomManager.AUDIO_OUTPUT_ENABLE_SPEAKER;
+ return SystemProperties.getInt(TelecomManager.PROPERTY_VIDEOCALL_AUDIO_OUTPUT,
+ TelecomManager.AUDIO_OUTPUT_DEFAULT) == TelecomManager.AUDIO_OUTPUT_ENABLE_SPEAKER;
}
/**
@@ -3223,10 +3481,10 @@
public void holdCall(Call call) {
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to be put on hold", call);
- } else {
- Log.d(this, "Putting call on hold: (%s)", call);
- call.hold();
+ return;
}
+ Log.d(this, "Putting call on hold: (%s)", call);
+ mCallSequencingAdapter.holdCall(call);
}
/**
@@ -3238,44 +3496,57 @@
public void unholdCall(Call call) {
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
- } else {
- if (getOutgoingCall() != null) {
- Log.w(this, "There is an outgoing call, so it is unable to unhold this call %s",
- call);
- return;
- }
- Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- String activeCallId = null;
- if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
- activeCallId = activeCall.getId();
- if (canHold(activeCall)) {
- activeCall.hold("Swap to " + call.getId());
- Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
- Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCall.getId());
- } else {
- // This call does not support hold. If it is from a different connection
- // service or connection manager, then disconnect it, otherwise invoke
- // call.hold() and allow the connection service or connection manager to handle
- // the situation.
- if (!areFromSameSource(activeCall, call)) {
- if (!activeCall.isEmergencyCall()) {
- activeCall.disconnect("Swap to " + call.getId());
- } else {
- Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
- activeCall.getId(), call.getId());
- // Don't unhold the call as requested; we don't want to drop an
- // emergency call.
- return;
- }
+ return;
+ }
+ if (getOutgoingCall() != null) {
+ Log.w(this, "There is an outgoing call, so it is unable to unhold this call %s",
+ call);
+ return;
+ }
+ mCallSequencingAdapter.unholdCall(call);
+ }
+
+ /**
+ * Instructs telecom to hold any ongoing active calls and bring this call to the active state.
+ * <p>
+ * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
+ */
+ public void unholdCallOld(Call call) {
+ Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+ String activeCallId = null;
+ if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
+ activeCallId = activeCall.getId();
+ if (canHold(activeCall)) {
+ activeCall.hold("Swap to " + call.getId());
+ Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
+ Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCall.getId());
+ } else {
+ // This call does not support hold. If it is from a different connection
+ // service or connection manager, then disconnect it, otherwise invoke
+ // call.hold() and allow the connection service or connection manager to handle
+ // the situation.
+ if (!areFromSameSource(activeCall, call)) {
+ if (!activeCall.isEmergencyCall()) {
+ activeCall.disconnect("Swap to " + call.getId());
} else {
- activeCall.hold("Swap to " + call.getId());
+ Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
+ activeCall.getId(), call.getId());
+ // Don't unhold the call as requested; we don't want to drop an
+ // emergency call.
+ return;
}
+ } else {
+ activeCall.hold("Swap to " + call.getId());
}
}
- mConnectionSvrFocusMgr.requestFocus(
- call,
- new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
}
+ requestActionUnholdCall(call, activeCallId);
+ }
+
+ public void requestActionUnholdCall(Call call, String activeCallId) {
+ mConnectionSvrFocusMgr.requestFocus(
+ call,
+ new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
}
@Override
@@ -3311,23 +3582,33 @@
// then include only that SIM based PhoneAccount and any non-SIM PhoneAccounts, such as SIP.
@VisibleForTesting
public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
- boolean isVideo, boolean isEmergency) {
- return constructPossiblePhoneAccounts(handle, user, isVideo, isEmergency, false);
+ boolean isVideo, boolean isEmergency, boolean isConference) {
+ if (mTelephonyFeatureFlags.simultaneousCallingIndications()) {
+ return constructPossiblePhoneAccountsNew(handle, user, isVideo, isEmergency,
+ isConference);
+ } else {
+ return constructPossiblePhoneAccountsOld(handle, user, isVideo, isEmergency,
+ isConference);
+ }
}
// 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;
}
}
- public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
+ private List<PhoneAccountHandle> constructPossiblePhoneAccountsOld(Uri handle, UserHandle user,
boolean isVideo, boolean isEmergency, boolean isConference) {
if (handle == null) {
@@ -3368,6 +3649,82 @@
return allAccounts;
}
+ /**
+ * Filters the list of all PhoneAccounts that match the outgoing call Handle's schema against
+ * the outgoing call request criteria and the state of the already ongoing calls on the
+ * device and their potential simultaneous calling restrictions.
+ * @return The filtered List
+ */
+ private List<PhoneAccountHandle> constructPossiblePhoneAccountsNew(Uri handle, UserHandle user,
+ boolean isVideo, boolean isEmergency, boolean isConference) {
+ if (handle == null) {
+ return Collections.emptyList();
+ }
+ // If we're specifically looking for video capable accounts, then include that capability,
+ // otherwise specify no additional capability constraints. When handling the emergency call,
+ // it also needs to find the phone accounts excluded by CAPABILITY_EMERGENCY_CALLS_ONLY.
+ int capabilities = isVideo ? PhoneAccount.CAPABILITY_VIDEO_CALLING : 0;
+ capabilities |= isConference ? PhoneAccount.CAPABILITY_ADHOC_CONFERENCE_CALLING : 0;
+ List<PhoneAccountHandle> allAccounts =
+ mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false, user,
+ capabilities,
+ isEmergency ? 0 : PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY,
+ isEmergency);
+ Log.v(this, "constructPossiblePhoneAccountsNew: allAccounts=" + allAccounts);
+ Set<PhoneAccountHandle> activeCallAccounts = mCalls.stream()
+ .filter(c -> !c.isDisconnected() && !c.isNew()).map(Call::getTargetPhoneAccount)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Log.v(this, "constructPossiblePhoneAccountsNew: activeCallAccounts="
+ + activeCallAccounts);
+ // No Active calls - all accounts are valid
+ if (activeCallAccounts.isEmpty()) return allAccounts;
+ // The emergency call should be attempted only over the same SIM PhoneAccounts where there
+ // are already ongoing calls - filter out inactive SIM PhoneAccounts in this case.
+ if (isEmergency) {
+ Set<PhoneAccountHandle> simAccounts =
+ new HashSet<>(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser());
+ if (activeCallAccounts.stream().anyMatch(simAccounts::contains)) {
+ allAccounts.removeIf(h -> {
+ boolean isRemoved = simAccounts.contains(h) && !activeCallAccounts.contains(h);
+ if (isRemoved) {
+ Log.i(this, "constructPossiblePhoneAccountsNew: removing candidate PAH ["
+ + h + "] because another SIM account is active with an emergency "
+ + "call");
+ }
+ return isRemoved;
+ });
+ }
+ }
+ // Apply restrictions to which PhoneAccounts can be used to place a call by looking at
+ // active calls and removing candidate PhoneAccounts if they are from the same source
+ // as the active call and the candidate PhoneAccount is not part of the restriction.
+ for (PhoneAccountHandle callHandle : activeCallAccounts) {
+ allAccounts.removeIf(candidateHandle -> {
+ PhoneAccount callAcct = mPhoneAccountRegistrar.getPhoneAccount(callHandle,
+ user);
+ if (callAcct == null) {
+ Log.w(this, "constructPossiblePhoneAccountsNew: unexpected"
+ + "null PA for PAH, removing : " + candidateHandle);
+ return true;
+ }
+ boolean isRemoved = !Objects.equals(candidateHandle, callHandle)
+ && Objects.equals(candidateHandle.getComponentName(),
+ callHandle.getComponentName())
+ && callAcct.hasSimultaneousCallingRestriction()
+ && !callAcct.getSimultaneousCallingRestriction().contains(candidateHandle);
+ if (isRemoved) {
+ Log.i(this, "constructPossiblePhoneAccountsNew: removing candidate"
+ + " PAH [" + candidateHandle + "] because it is not part of the"
+ + " restriction set by [" + callHandle + "], restriction="
+ + callAcct.getSimultaneousCallingRestriction());
+ }
+ return isRemoved;
+ });
+ }
+ return allAccounts;
+ }
+
private TelephonyManager getTelephonyManager() {
return mContext.getSystemService(TelephonyManager.class);
}
@@ -3418,7 +3775,7 @@
}
/** Called by the in-call UI to change the mute state. */
- void mute(boolean shouldMute) {
+ public void mute(boolean shouldMute) {
if (isInEmergencyCall() && shouldMute) {
Log.i(this, "Refusing to turn on mute because we're in an emergency call");
shouldMute = false;
@@ -3470,6 +3827,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;
}
@@ -3528,14 +3886,15 @@
* Called when disconnect tone is started or stopped, including any InCallTone
* after disconnected call.
*
+ * @param call
* @param isTonePlaying true if the disconnected tone is started, otherwise the disconnected
- * tone is stopped.
+ * tone is stopped.
*/
@VisibleForTesting
- public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+ public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
Log.v(this, "onDisconnectedTonePlaying, %s", isTonePlaying ? "started" : "stopped");
for (CallsManagerListener listener : mListeners) {
- listener.onDisconnectedTonePlaying(isTonePlaying);
+ listener.onDisconnectedTonePlaying(call, isTonePlaying);
}
}
@@ -3556,6 +3915,11 @@
maybeMoveToSpeakerPhone(call);
}
+ void requestFocusActionAnswerCall(Call call, int videoState) {
+ mConnectionSvrFocusMgr.requestFocus(call, new CallsManager.RequestCallback(
+ new CallsManager.ActionAnswerCall(call, videoState)));
+ }
+
/**
* Returns true if the active call is held.
*/
@@ -3567,8 +3931,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
@@ -3617,45 +3980,93 @@
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);
+ }
+
+ /**
+ * CS: Mark a call as active. If the call is self-mangaed, we will also hold any active call
+ * before moving the self-managed call to active.
+ * <p>
+ * Note: Only used when {@link FeatureFlags#enableCallSequencing()} is false.
+ */
@VisibleForTesting
public void markCallAsActive(Call call) {
Log.i(this, "markCallAsActive, isSelfManaged: " + call.isSelfManaged());
@@ -3690,6 +4101,11 @@
}
}
+ /**
+ * Mark a call as on hold after the hold operation has already completed.
+ * <p>
+ * Note: only used when {@link FeatureFlags#enableCallSequencing()} is false.
+ */
public void markCallAsOnHold(Call call) {
setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
}
@@ -3730,11 +4146,6 @@
// Notify listeners that the call was disconnected before being added to CallsManager.
// Listeners will not receive onAdded or onRemoved callbacks.
if (!mCalls.contains(call)) {
- if (call.isEmergencyCall()) {
- mAnomalyReporter.reportAnomaly(
- EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID,
- EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG);
- }
mListeners.forEach(l -> l.onCreateConnectionFailed(call));
}
@@ -3752,20 +4163,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.
@@ -3785,16 +4197,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);
@@ -3802,8 +4205,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) {
@@ -3813,6 +4260,10 @@
}, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
.exceptionally((throwable) -> {
Log.e(TAG, throwable, "Error while executing call removal");
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_REMOVING_CALL);
+ }
mAnomalyReporter.reportAnomaly(CALL_REMOVAL_EXECUTION_ERROR_UUID,
CALL_REMOVAL_EXECUTION_ERROR_MSG);
return null;
@@ -3830,12 +4281,24 @@
private void doRemoval(Call call) {
call.maybeCleanupHandover();
removeCall(call);
+ boolean isLocallyDisconnecting = mLocallyDisconnectingCalls.contains(call);
+ mLocallyDisconnectingCalls.remove(call);
+ mCallSequencingAdapter.unholdCallForRemoval(call, isLocallyDisconnecting);
+ }
+
+ /**
+ * Move the held call to foreground in the event that there is a held call and the disconnected
+ * call was disconnected locally or the held call has no way to auto-unhold because it does not
+ * support hold capability.
+ * <p>
+ * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is set to false.
+ */
+ public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
- if (mLocallyDisconnectingCalls.contains(call)) {
- boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
- Log.v(this, "performRemoval: isDisconnectingChildCall = "
- + isDisconnectingChildCall + "call -> %s", call);
- mLocallyDisconnectingCalls.remove(call);
+ if (isLocallyDisconnecting) {
+ boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
+ Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
+ + isDisconnectingChildCall + "call -> %s", removedCall);
// Auto-unhold the foreground call due to a locally disconnected call, except if the
// call which was disconnected is a member of a conference (don't want to auto
// un-hold the conference if we remove a member of the conference).
@@ -3844,7 +4307,8 @@
// implementations, especially if one is managed and the other is a VoIP CS.
if (!isDisconnectingChildCall && foregroundCall != null
&& foregroundCall.getState() == CallState.ON_HOLD
- && areFromSameSource(foregroundCall, call)) {
+ && areFromSameSource(foregroundCall, removedCall)) {
+
foregroundCall.unhold();
}
} else if (foregroundCall != null &&
@@ -3854,8 +4318,8 @@
// The new foreground call is on hold, however the carrier does not display the hold
// button in the UI. Therefore, we need to auto unhold the held call since the user
// has no means of unholding it themselves.
- Log.i(this, "performRemoval: Auto-unholding held foreground call (call doesn't "
- + "support hold)");
+ Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
+ + "doesn't support hold)");
foregroundCall.unhold();
}
}
@@ -3962,12 +4426,14 @@
return true;
}
} else {
+ Log.addEvent(ringingCall, LogUtils.Events.INFO,
+ "media btn short press - answer call.");
answerCall(ringingCall, VideoProfile.STATE_AUDIO_ONLY);
return true;
}
} else if (HeadsetMediaButton.LONG_PRESS == type) {
if (ringingCall != null) {
- Log.addEvent(getForegroundCall(),
+ Log.addEvent(ringingCall,
LogUtils.Events.INFO, "media btn long press - reject");
ringingCall.reject(false, null);
} else {
@@ -3988,6 +4454,7 @@
return true;
}
}
+ Log.i(this, "onMediaButton: type=%d; no active calls", type);
return false;
}
@@ -4079,6 +4546,10 @@
return getFirstCallWithState(null, states);
}
+ public Call getFirstCallWithLiveState() {
+ return getFirstCallWithState(null, LIVE_CALL_STATES);
+ }
+
@VisibleForTesting
public PhoneNumberUtilsAdapter getPhoneNumberUtilsAdapter() {
return mPhoneNumberUtilsAdapter;
@@ -4169,7 +4640,8 @@
connectTime,
connectElapsedTime,
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
// Unlike connections, conferences are not created first and then notified as create
// connection complete from the CS. They originate from the CS and are reported directly to
@@ -4187,7 +4659,10 @@
call.setStatusHints(parcelableConference.getStatusHints());
call.putConnectionServiceExtras(parcelableConference.getExtras());
// For conference calls, set the associated user from the target phone account user handle.
- call.setAssociatedUser(phoneAccount.getUserHandle());
+ UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+ mFeatureFlags.associatedUserRefactorForWorkProfile(), getPhoneAccountRegistrar(),
+ getCurrentUserHandle(), phoneAccount);
+ call.setAssociatedUser(associatedUser);
// In case this Conference was added via a ConnectionManager, keep track of the original
// Connection ID as created by the originating ConnectionService.
Bundle extras = parcelableConference.getExtras();
@@ -4276,7 +4751,6 @@
Log.i(this, "addCall(%s) is already added");
return;
}
- Trace.beginSection("addCall");
Log.i(this, "addCall(%s)", call);
call.addListener(this);
mCalls.add(call);
@@ -4293,20 +4767,12 @@
updateExternalCallCanPullSupport();
// onCallAdded for calls which immediately take the foreground (like the first call).
for (CallsManagerListener listener : mListeners) {
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.beginSection(listener.getClass().toString() + " addCall");
- }
listener.onCallAdded(call);
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.endSection();
- }
}
- Trace.endSection();
}
@VisibleForTesting
public void removeCall(Call call) {
- Trace.beginSection("removeCall");
Log.v(this, "removeCall(%s)", call);
if (call.isTransactionalCall() && call.getTransactionServiceWrapper() != null) {
@@ -4333,16 +4799,9 @@
updateCanAddCall();
updateHasActiveRttCall();
for (CallsManagerListener listener : mListeners) {
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.beginSection(listener.getClass().toString() + " onCallRemoved");
- }
listener.onCallRemoved(call);
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.endSection();
- }
}
}
- Trace.endSection();
}
private void updateHasActiveRttCall() {
@@ -4405,13 +4864,8 @@
call.getAnalytics().setMissedReason(call.getMissedReason());
maybeShowErrorDialogOnDisconnect(call);
-
- Trace.beginSection("onCallStateChanged");
-
maybeHandleHandover(call, newState);
notifyCallStateChanged(call, oldState, newState);
-
- Trace.endSection();
} else {
Log.i(this, "failed in setting the state to new state");
}
@@ -4424,14 +4878,7 @@
updateCanAddCall();
updateHasActiveRttCall();
for (CallsManagerListener listener : mListeners) {
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.beginSection(listener.getClass().toString() +
- " onCallStateChanged");
- }
listener.onCallStateChanged(call, oldState, newState);
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.endSection();
- }
}
}
}
@@ -4473,10 +4920,6 @@
if (handoverState == HandoverState.HANDOVER_FROM_STARTED) {
// Disconnect before handover was accepted.
Log.i(this, "setCallState: disconnect before handover accepted");
- // Let the handover destination know that the source has disconnected prior to
- // completion of the handover.
- call.getHandoverDestinationCall().sendCallEvent(
- android.telecom.Call.EVENT_HANDOVER_SOURCE_DISCONNECTED, null);
} else if (handoverState == HandoverState.HANDOVER_ACCEPTED) {
Log.i(this, "setCallState: handover from complete");
completeHandoverFrom(call);
@@ -4494,11 +4937,9 @@
// Inform the "from" Call (ie the source call) that the handover from it has
// completed; this allows the InCallService to be notified that a handover it
// initiated completed.
- call.onConnectionEvent(Connection.EVENT_HANDOVER_COMPLETE, null);
call.onHandoverComplete();
// Inform the "to" ConnectionService that handover to it has completed.
- handoverTo.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_COMPLETE, null);
handoverTo.onHandoverComplete();
answerCall(handoverTo, handoverTo.getVideoState());
call.markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
@@ -4521,7 +4962,6 @@
// Inform the "from" Call (ie the source call) that the handover from it has
// failed; this allows the InCallService to be notified that a handover it
// initiated failed.
- handoverFrom.onConnectionEvent(Connection.EVENT_HANDOVER_FAILED, null);
handoverFrom.onHandoverFailed(android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
// Inform the "to" ConnectionService that handover to it has failed. This
@@ -4530,7 +4970,6 @@
// Only attempt if the call has a bound ConnectionService if handover failed
// early on in the handover process, the CS will be unbound and we won't be
// able to send the call event.
- handoverTo.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
handoverTo.getConnectionService().handoverFailed(handoverTo,
android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
}
@@ -4564,30 +5003,29 @@
if (newCanAddCall != mCanAddCall) {
mCanAddCall = newCanAddCall;
for (CallsManagerListener listener : mListeners) {
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.beginSection(listener.getClass().toString() + " updateCanAddCall");
- }
listener.onCanAddCallChanged(mCanAddCall);
- if (LogUtils.SYSTRACE_DEBUG) {
- Trace.endSection();
- }
}
}
}
/**
- * Determines if there are any ongoing self managed calls for the given package/user.
+ * Determines if there are any ongoing self-managed calls for the given package/user.
* @param packageName The package name to check.
- * @param userHandle The userhandle to check.
+ * @param userHandle The {@link UserHandle} to check.
* @return {@code true} if the app has ongoing calls, or {@code false} otherwise.
*/
public boolean isInSelfManagedCall(String packageName, UserHandle userHandle) {
+ boolean hasCrossUserAccess = userHandle.equals(UserHandle.ALL);
return mSelfManagedCallsBeingSetup.stream().anyMatch(c -> c.isSelfManaged()
&& c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
- && c.getTargetPhoneAccount().getUserHandle().equals(userHandle)) ||
- mCalls.stream().anyMatch(c -> c.isSelfManaged()
+ && (!hasCrossUserAccess
+ ? c.getTargetPhoneAccount().getUserHandle().equals(userHandle)
+ : true))
+ || mCalls.stream().anyMatch(c -> c.isSelfManaged()
&& c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
- && c.getTargetPhoneAccount().getUserHandle().equals(userHandle));
+ && (!hasCrossUserAccess
+ ? c.getTargetPhoneAccount().getUserHandle().equals(userHandle)
+ : true));
}
@VisibleForTesting
@@ -4693,7 +5131,7 @@
return (int) callsStream.count();
}
- private boolean hasMaximumLiveCalls(Call exceptCall) {
+ public boolean hasMaximumLiveCalls(Call exceptCall) {
return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
exceptCall, null /* phoneAccountHandle*/, LIVE_CALL_STATES);
}
@@ -4766,6 +5204,7 @@
// change what an "active call" is so that the call in SELECT_PHONE_ACCOUNT state
// will be properly cancelled.
call.getTargetPhoneAccount() != null
+ && phoneAccountHandle != null
&& !phoneAccountHandle.getComponentName().equals(
call.getTargetPhoneAccount().getComponentName())
&& call.getParentCall() == null
@@ -4836,6 +5275,14 @@
&& incomingCall.getHandoverSourceCall() == null;
}
+ /**
+ * Make room for a pending outgoing emergency {@link Call}.
+ * <p>
+ * Note: This method is only applicable when {@link FeatureFlags#enableCallSequencing()}
+ * is false.
+ * @param call The new pending outgoing call.
+ * @return true if room was made, false if no room could be made.
+ */
@VisibleForTesting
public boolean makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
// Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
@@ -4912,6 +5359,10 @@
// If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
if (liveCall.getState() == CallState.CONNECTING) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_STUCK_CONNECTING_EMERGENCY);
+ }
mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID,
LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG);
}
@@ -4998,6 +5449,14 @@
return false;
}
+ /**
+ * Make room for a pending outgoing {@link Call}.
+ * <p>
+ * Note: This method is only applicable when {@link FeatureFlags#enableCallSequencing()}
+ * is false.
+ * @param call The new pending outgoing call.
+ * @return true if room was made, false if no room could be made.
+ */
@VisibleForTesting
public boolean makeRoomForOutgoingCall(Call call) {
// Already room!
@@ -5028,6 +5487,10 @@
if (liveCall.getState() == CallState.CONNECTING
&& ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
> mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.ERROR_STUCK_CONNECTING);
+ }
mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
liveCall.disconnect("Force disconnect CONNECTING call.");
@@ -5225,7 +5688,8 @@
connection.getConnectTimeMillis() /* connectTimeMillis */,
connection.getConnectElapsedTimeMillis(), /* connectElapsedTimeMillis */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
call.initAnalytics();
call.getAnalytics().setCreatedFromExistingConnection(true);
@@ -5240,7 +5704,10 @@
connection.getCallerDisplayNamePresentation());
// For existing connections, use the phone account user handle to determine the user
// association with the call.
- call.setAssociatedUser(connection.getPhoneAccount().getUserHandle());
+ UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+ mFeatureFlags.associatedUserRefactorForWorkProfile(), getPhoneAccountRegistrar(),
+ getCurrentUserHandle(), connection.getPhoneAccount());
+ call.setAssociatedUser(associatedUser);
call.addListener(this);
call.putConnectionServiceExtras(connection.getExtras());
@@ -5323,10 +5790,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());
+ }
}
}
@@ -5335,7 +5813,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);
}
}
@@ -5444,8 +5922,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",
@@ -5465,7 +5945,7 @@
mCallAudioManager.getCallAudioModeStateMachine().getHandler().post(() -> {
mainHandlerLatch.countDown();
});
- mCallAudioManager.getCallAudioRouteStateMachine().getHandler().post(() -> {
+ mCallAudioManager.getCallAudioRouteAdapter().getAdapterHandler().post(() -> {
mainHandlerLatch.countDown();
});
@@ -5491,9 +5971,10 @@
// We are going to place the new outgoing call, so disconnect any ongoing self-managed
// calls which are ongoing at this time.
disconnectSelfManagedCalls("outgoing call " + callId);
-
- mPendingCallConfirm.complete(mPendingCall);
- mPendingCallConfirm = null;
+ if (mPendingCallConfirm != null) {
+ mPendingCallConfirm.complete(mPendingCall);
+ mPendingCallConfirm = null;
+ }
mPendingCall = null;
}
}
@@ -5512,8 +5993,10 @@
markCallAsDisconnected(mPendingCall, new DisconnectCause(DisconnectCause.CANCELED));
markCallAsRemoved(mPendingCall);
mPendingCall = null;
- mPendingCallConfirm.complete(null);
- mPendingCallConfirm = null;
+ if (mPendingCallConfirm != null) {
+ mPendingCallConfirm.complete(null);
+ mPendingCallConfirm = null;
+ }
}
}
@@ -5815,28 +6298,6 @@
}
/**
- * Called in response to a {@link Call} receiving a {@link Call#sendCallEvent(String, Bundle)}
- * of type {@link android.telecom.Call#EVENT_REQUEST_HANDOVER} indicating the
- * {@link android.telecom.InCallService} has requested a handover to another
- * {@link android.telecom.ConnectionService}.
- *
- * We will explicitly disallow a handover when there is an emergency call present.
- *
- * @param handoverFromCall The {@link Call} to be handed over.
- * @param handoverToHandle The {@link PhoneAccountHandle} to hand over the call to.
- * @param videoState The desired video state of {@link Call} after handover.
- * @param initiatingExtras Extras associated with the handover, to be passed to the handover
- * {@link android.telecom.ConnectionService}.
- */
- private void requestHandoverViaEvents(Call handoverFromCall,
- PhoneAccountHandle handoverToHandle,
- int videoState, Bundle initiatingExtras) {
-
- handoverFromCall.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
- Log.addEvent(handoverFromCall, LogUtils.Events.HANDOVER_REQUEST, "legacy request denied");
- }
-
- /**
* Called in response to a {@link Call} receiving a {@link Call#handoverTo(PhoneAccountHandle,
* int, Bundle)} indicating the {@link android.telecom.InCallService} has requested a
* handover to another {@link android.telecom.ConnectionService}.
@@ -5882,7 +6343,7 @@
handoverFromCall.getHandle(), null,
null, null,
Call.CALL_DIRECTION_OUTGOING, false,
- false, mClockProxy, mToastFactory);
+ false, mClockProxy, mToastFactory, mFeatureFlags);
call.initAnalytics();
// Set self-managed and voipAudioMode if destination is self-managed CS
@@ -6089,7 +6550,8 @@
false /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
if (fromCall == null || isHandoverInProgress() ||
!isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount()) ||
@@ -6158,7 +6620,7 @@
call.can(Connection.CAPABILITY_HOLD)) && call.getState() != CallState.DIALING;
}
- private boolean supportsHold(Call call) {
+ public boolean supportsHold(Call call) {
return call.can(Connection.CAPABILITY_SUPPORT_HOLD);
}
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 43f3b90..0c54be3 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -16,10 +16,10 @@
package com.android.server.telecom;
-import android.telecom.AudioState;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
import android.telecom.VideoProfile;
+
import java.util.Set;
/**
@@ -112,7 +112,7 @@
}
@Override
- public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+ public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
}
@Override
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index 3694727..35be0f8 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -26,8 +26,11 @@
import android.telecom.Logging.Session;
import android.text.TextUtils;
import android.util.LocalLog;
+import android.util.LogPrinter;
+import android.util.Printer;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.Flags;
import com.android.internal.util.IndentingPrintWriter;
import java.util.ArrayList;
@@ -35,6 +38,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -44,6 +48,11 @@
private static final String TAG = "ConnectionSvrFocusMgr";
private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000;
private final LocalLog mLocalLog = new LocalLog(20);
+ private final AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+ public static final UUID WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID =
+ UUID.fromString("edd7334a-ef87-432b-a1d0-a2f23959c73e");
+ public static final String WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG =
+ "Telecom CallAnomalyWatchdog detected a timeout while getting the call focus.";
/** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */
public interface ConnectionServiceFocusManagerFactory {
@@ -332,8 +341,23 @@
if (syncCallFocus != null) {
return syncCallFocus.orElse(null);
} else {
- Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
- + " inaccurate result");
+ if (Flags.genAnomReportOnFocusTimeout()) {
+ Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
+ + " inaccurate result. returning currentFocusCall=[%s]",
+ mCurrentFocusCall);
+
+ // dump the state of the handler to better understand the timeout
+ mEventHandler.dump(
+ new LogPrinter(android.util.Log.INFO, TAG), "CsFocusMgr_timeout");
+
+ // report the timeout
+ mAnomalyReporter.reportAnomaly(
+ WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID,
+ WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG);
+ } else {
+ Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
+ + " inaccurate result");
+ }
return mCurrentFocusCall;
}
} catch (InterruptedException e) {
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index 3991ed5..e4ed220 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -23,6 +23,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.HashMap;
@@ -37,6 +38,7 @@
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
+ private final FeatureFlags mFeatureFlags;
private final ServiceBinder.Listener<ConnectionServiceWrapper> mUnbindListener =
new ServiceBinder.Listener<ConnectionServiceWrapper>() {
@@ -53,15 +55,19 @@
PhoneAccountRegistrar phoneAccountRegistrar,
Context context,
TelecomSystem.SyncRoot lock,
- CallsManager callsManager) {
+ CallsManager callsManager,
+ FeatureFlags featureFlags) {
mPhoneAccountRegistrar = phoneAccountRegistrar;
mContext = context;
mLock = lock;
mCallsManager = callsManager;
+ mFeatureFlags = featureFlags;
}
@VisibleForTesting
- public ConnectionServiceWrapper getService(ComponentName componentName, UserHandle userHandle) {
+ public ConnectionServiceWrapper getService(
+ ComponentName componentName,
+ UserHandle userHandle) {
Pair<ComponentName, UserHandle> cacheKey = Pair.create(componentName, userHandle);
ConnectionServiceWrapper service = mServiceCache.get(cacheKey);
if (service == null) {
@@ -72,7 +78,8 @@
mCallsManager,
mContext,
mLock,
- userHandle);
+ userHandle,
+ mFeatureFlags);
service.addListener(mUnbindListener);
mServiceCache.put(cacheKey, service);
}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 57b7091..260c238 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -23,17 +23,16 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
import android.location.Location;
import android.location.LocationManager;
import android.location.LocationRequest;
-import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
-import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
@@ -66,8 +65,10 @@
import com.android.internal.telecom.IVideoProvider;
import com.android.internal.telecom.RemoteServiceCallback;
import com.android.internal.util.Preconditions;
+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.List;
@@ -91,7 +92,7 @@
*/
@VisibleForTesting
public class ConnectionServiceWrapper extends ServiceBinder implements
- ConnectionServiceFocusManager.ConnectionServiceFocus {
+ ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
/**
* Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
@@ -129,11 +130,7 @@
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
Call call = mCallIdMapper.getCall(callId);
- if (call != null && mScheduledFutureMap.containsKey(call)) {
- ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
- existingTimeout.cancel(false /* cancelIfRunning */);
- mScheduledFutureMap.remove(call);
- }
+ maybeRemoveCleanupFuture(call);
// Check status hints image for cross user access
if (connection.getStatusHints() != null) {
Icon icon = connection.getStatusHints().getIcon();
@@ -173,11 +170,7 @@
synchronized (mLock) {
logIncoming("handleCreateConferenceComplete %s", callId);
Call call = mCallIdMapper.getCall(callId);
- if (call != null && mScheduledFutureMap.containsKey(call)) {
- ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
- existingTimeout.cancel(false /* cancelIfRunning */);
- mScheduledFutureMap.remove(call);
- }
+ maybeRemoveCleanupFuture(call);
// Check status hints image for cross user access
if (conference.getStatusHints() != null) {
Icon icon = conference.getStatusHints().getIcon();
@@ -421,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));
}
@@ -557,8 +555,8 @@
// Check status hints image for cross user access
if (parcelableConference.getStatusHints() != null) {
Icon icon = parcelableConference.getStatusHints().getIcon();
- parcelableConference.getStatusHints().setIcon(StatusHints.
- validateAccountIconUserBoundary(icon, callingUserHandle));
+ parcelableConference.getStatusHints().setIcon(StatusHints
+ .validateAccountIconUserBoundary(icon, callingUserHandle));
}
if (parcelableConference.getConnectElapsedTimeMillis() != 0
@@ -1033,6 +1031,12 @@
connectIdToCheck = callId;
}
+ // Check status hints image for cross user access
+ if (connection.getStatusHints() != null) {
+ Icon icon = connection.getStatusHints().getIcon();
+ connection.getStatusHints().setIcon(StatusHints.
+ validateAccountIconUserBoundary(icon, userHandle));
+ }
// Handle the case where an existing connection was added by Telephony via
// a connection manager. The remote connection service API does not include
// the ability to specify a parent connection when adding an existing
@@ -1071,14 +1075,6 @@
connection.getCallDirection(),
connection.getCallerNumberVerificationStatus());
}
-
- // Check status hints image for cross user access
- if (connection.getStatusHints() != null) {
- Icon icon = connection.getStatusHints().getIcon();
- connection.getStatusHints().setIcon(StatusHints.
- validateAccountIconUserBoundary(icon, userHandle));
- }
-
// Check to see if this Connection has already been added.
Call alreadyAddedConnection = mCallsManager
.getAlreadyAddedConnection(connectIdToCheck);
@@ -1412,8 +1408,10 @@
CallsManager callsManager,
Context context,
TelecomSystem.SyncRoot lock,
- UserHandle userHandle) {
- super(ConnectionService.SERVICE_INTERFACE, componentName, context, lock, userHandle);
+ UserHandle userHandle,
+ FeatureFlags featureFlags) {
+ super(ConnectionService.SERVICE_INTERFACE, componentName, context, lock, userHandle,
+ featureFlags);
mConnectionServiceRepository = connectionServiceRepository;
phoneAccountRegistrar.addListener(new PhoneAccountRegistrar.Listener() {
// TODO -- Upon changes to PhoneAccountRegistrar, need to re-wire connections
@@ -1447,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;
}
@@ -1667,9 +1668,11 @@
call.shouldAttachToExistingConnection(),
call.isUnknown(),
Log.getExternalSession(TELECOM_ABBREVIATION));
-
} catch (RemoteException e) {
Log.e(this, e, "Failure to createConference -- %s", getComponentName());
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mPendingResponses.remove(callId).handleCreateConferenceFailure(
new DisconnectCause(DisconnectCause.ERROR, e.toString()));
}
@@ -1700,6 +1703,9 @@
Log.i(ConnectionServiceWrapper.this, "Call not present"
+ " in call id mapper, maybe it was aborted before the bind"
+ " completed successfully?");
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
response.handleCreateConnectionFailure(
new DisconnectCause(DisconnectCause.CANCELED));
return;
@@ -1784,16 +1790,27 @@
SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
mScheduledFutureMap.put(call, future);
try {
- mServiceInterface.createConnection(
- call.getConnectionManagerPhoneAccount(),
- callId,
- connectionRequest,
- call.shouldAttachToExistingConnection(),
- call.isUnknown(),
- Log.getExternalSession(TELECOM_ABBREVIATION));
-
+ if (mFlags.cswServiceInterfaceIsNull() && mServiceInterface == null) {
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
+ mPendingResponses.remove(callId).handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.ERROR,
+ "CSW#oCC ServiceInterface is null"));
+ } else {
+ mServiceInterface.createConnection(
+ call.getConnectionManagerPhoneAccount(),
+ callId,
+ connectionRequest,
+ call.shouldAttachToExistingConnection(),
+ call.isUnknown(),
+ Log.getExternalSession(TELECOM_ABBREVIATION));
+ }
} catch (RemoteException e) {
Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mPendingResponses.remove(callId).handleCreateConnectionFailure(
new DisconnectCause(DisconnectCause.ERROR, e.toString()));
}
@@ -2038,6 +2055,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")) {
@@ -2053,6 +2071,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);
@@ -2069,8 +2088,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")) {
@@ -2113,7 +2138,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 {
@@ -2264,6 +2290,9 @@
if (response != null) {
response.handleCreateConnectionFailure(disconnectCause);
}
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(mCallIdMapper.getCall(callId));
+ }
mCallIdMapper.removeCall(callId);
}
@@ -2273,6 +2302,9 @@
if (response != null) {
response.handleCreateConnectionFailure(disconnectCause);
}
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mCallIdMapper.removeCall(call);
}
@@ -2363,7 +2395,8 @@
}
}
- void sendCallEvent(Call call, String event, Bundle extras) {
+ @Override
+ public void sendCallEvent(Call call, String event, Bundle extras) {
final String callId = mCallIdMapper.getCallId(call);
if (callId != null && isServiceValid("sendCallEvent")) {
try {
@@ -2488,6 +2521,7 @@
BindCallback callback = new BindCallback() {
@Override
public void onSuccess() {
+ if (!isServiceValid("connectionServiceFocusLost")) return;
try {
mServiceInterface.connectionServiceFocusLost(
Log.getExternalSession(TELECOM_ABBREVIATION));
@@ -2507,6 +2541,7 @@
BindCallback callback = new BindCallback() {
@Override
public void onSuccess() {
+ if (!isServiceValid("connectionServiceFocusGained")) return;
try {
mServiceInterface.connectionServiceFocusGained(
Log.getExternalSession(TELECOM_ABBREVIATION));
@@ -2585,12 +2620,11 @@
*/
private void handleConnectionServiceDeath() {
if (!mPendingResponses.isEmpty()) {
- CreateConnectionResponse[] responses = mPendingResponses.values().toArray(
- new CreateConnectionResponse[mPendingResponses.values().size()]);
+ Collection<CreateConnectionResponse> responses = mPendingResponses.values();
mPendingResponses.clear();
- for (int i = 0; i < responses.length; i++) {
- responses[i].handleCreateConnectionFailure(
- new DisconnectCause(DisconnectCause.ERROR, "CS_DEATH"));
+ for (CreateConnectionResponse response : responses) {
+ response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.ERROR,
+ "CS_DEATH"));
}
}
mCallIdMapper.clear();
@@ -2645,9 +2679,11 @@
}
}
- // Bail early if the caller isn't the sim connection mgr.
- if (!isCallerConnectionManager) {
- Log.d(this, "queryRemoteConnectionServices: none; not sim call mgr.");
+ Log.i(this, "queryRemoteConnectionServices, simServices = %s", simServices);
+ // Bail early if the caller isn't the sim connection mgr or no sim connection service
+ // other than caller available.
+ if (!isCallerConnectionManager || simServices.isEmpty()) {
+ Log.d(this, "queryRemoteConnectionServices: not sim call mgr or no simservices.");
noRemoteServices(callback);
return;
}
@@ -2655,8 +2691,6 @@
final List<ComponentName> simServiceComponentNames = new ArrayList<>();
final List<IBinder> simServiceBinders = new ArrayList<>();
- Log.i(this, "queryRemoteConnectionServices, simServices = %s", simServices);
-
for (ConnectionServiceWrapper simService : simServices) {
final ConnectionServiceWrapper currentSimService = simService;
@@ -2730,4 +2764,20 @@
public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
mAnomalyReporter = mAnomalyReporterAdapter;
}
+
+ /**
+ * Given a call, unschedule and cancel the cleanup future.
+ * @param call the call.
+ */
+ private void maybeRemoveCleanupFuture(Call call) {
+ if (call == null) {
+ return;
+ }
+ ScheduledFuture<?> future = mScheduledFutureMap.remove(call);
+ if (future == null) {
+ return;
+ }
+ future.cancel(false /* interrupt */);
+
+ }
}
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index dea070c..a2c742d 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -32,6 +32,8 @@
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.Flags;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.ArrayList;
import java.util.Collection;
@@ -101,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;
@@ -125,14 +137,20 @@
private DisconnectCause mLastErrorDisconnectCause;
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) {
+ 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;
@@ -140,6 +158,8 @@
mPhoneAccountRegistrar = phoneAccountRegistrar;
mContext = context;
mConnectionAttempt = 0;
+ mFlags = featureFlags;
+ mTimeoutsAdapter = timeoutsAdapter;
}
boolean isProcessingComplete() {
@@ -247,20 +267,24 @@
mConnectionAttempt++;
mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
- if (Objects.equals(attempt.connectionManagerPhoneAccount,
- attempt.targetPhoneAccount)) {
- mCall.setConnectionService(mService);
- } else {
- PhoneAccountHandle remotePhoneAccount = attempt.targetPhoneAccount;
- ConnectionServiceWrapper mRemoteService =
- mRepository.getService(remotePhoneAccount.getComponentName(),
- remotePhoneAccount.getUserHandle());
- if (mRemoteService == null) {
+ if (mFlags.updatedRcsCallCountTracking()) {
+ if (Objects.equals(attempt.connectionManagerPhoneAccount,
+ attempt.targetPhoneAccount)) {
mCall.setConnectionService(mService);
} else {
- Log.v(this, "attemptNextPhoneAccount Setting RCS = %s", mRemoteService);
- mCall.setConnectionService(mService, mRemoteService);
+ PhoneAccountHandle remotePhoneAccount = attempt.targetPhoneAccount;
+ ConnectionServiceWrapper mRemoteService =
+ mRepository.getService(remotePhoneAccount.getComponentName(),
+ remotePhoneAccount.getUserHandle());
+ if (mRemoteService == null) {
+ mCall.setConnectionService(mService);
+ } else {
+ Log.v(this, "attemptNextPhoneAccount Setting RCS = %s", mRemoteService);
+ mCall.setConnectionService(mService, mRemoteService);
+ }
}
+ } else {
+ mCall.setConnectionService(mService);
}
setTimeoutIfNeeded(mService, attempt);
if (mCall.isIncoming()) {
@@ -308,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 dc79715..98289ed 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -31,76 +31,58 @@
import android.provider.Settings;
import android.telecom.DefaultDialerManager;
import android.telecom.Log;
-import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;
public class DefaultDialerCache {
- public interface DefaultDialerManagerAdapter {
- String getDefaultDialerApplication(Context context);
- String getDefaultDialerApplication(Context context, int userId);
- boolean setDefaultDialerApplication(Context context, String packageName, int userId);
- }
-
- static class DefaultDialerManagerAdapterImpl implements DefaultDialerManagerAdapter {
- @Override
- public String getDefaultDialerApplication(Context context) {
- return DefaultDialerManager.getDefaultDialerApplication(context);
- }
-
- @Override
- public String getDefaultDialerApplication(Context context, int userId) {
- return DefaultDialerManager.getDefaultDialerApplication(context, userId);
- }
-
- @Override
- public boolean setDefaultDialerApplication(Context context, String packageName,
- int userId) {
- return DefaultDialerManager.setDefaultDialerApplication(context, packageName, userId);
- }
- }
-
private static final String LOG_TAG = "DefaultDialerCache";
+ @VisibleForTesting
+ public final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Context mContext;
+ private final DefaultDialerManagerAdapter mDefaultDialerManagerAdapter;
+ private final ComponentName mSystemDialerComponentName;
+ private final RoleManagerAdapter mRoleManagerAdapter;
+ private final ConcurrentHashMap<Integer, String> mCurrentDefaultDialerPerUser =
+ new ConcurrentHashMap<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- Log.startSession("DDC.oR");
- try {
- String packageName;
- if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
- packageName = null;
- } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())
- && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- packageName = intent.getData().getSchemeSpecificPart();
- } else if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
- packageName = null;
- } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- packageName = null;
- } else {
- return;
- }
+ mHandler.post(() -> {
+ Log.startSession("DDC.oR");
+ try {
+ String packageName;
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
+ packageName = null;
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())
+ && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ packageName = intent.getData().getSchemeSpecificPart();
+ } else if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
+ packageName = null;
+ } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ packageName = null;
+ } else {
+ return;
+ }
- synchronized (mLock) {
refreshCachesForUsersWithPackage(packageName);
+ } finally {
+ Log.endSession();
}
-
- } finally {
- Log.endSession();
- }
+ });
}
};
-
private final BroadcastReceiver mUserRemovedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
int removedUser = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
- UserHandle.USER_NULL);
+ UserHandle.USER_NULL);
if (removedUser == UserHandle.USER_NULL) {
Log.w(LOG_TAG, "Expected EXTRA_USER_HANDLE with ACTION_USER_REMOVED");
} else {
@@ -110,8 +92,6 @@
}
}
};
-
- private final Handler mHandler = new Handler(Looper.getMainLooper());
private final ContentObserver mDefaultDialerObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -119,9 +99,7 @@
try {
// We don't get the user ID of the user that changed here, so we'll have to
// refresh all of the users.
- synchronized (mLock) {
- refreshCachesForUsersWithPackage(null);
- }
+ refreshCachesForUsersWithPackage(null);
} finally {
Log.endSession();
}
@@ -132,13 +110,6 @@
return true;
}
};
-
- private final Context mContext;
- private final DefaultDialerManagerAdapter mDefaultDialerManagerAdapter;
- private final TelecomSystem.SyncRoot mLock;
- private final ComponentName mSystemDialerComponentName;
- private final RoleManagerAdapter mRoleManagerAdapter;
- private SparseArray<String> mCurrentDefaultDialerPerUser = new SparseArray<>();
private ComponentName mOverrideSystemDialerComponentName;
public DefaultDialerCache(Context context,
@@ -148,13 +119,12 @@
mContext = context;
mDefaultDialerManagerAdapter = defaultDialerManagerAdapter;
mRoleManagerAdapter = roleManagerAdapter;
- mLock = lock;
+
Resources resources = mContext.getResources();
mSystemDialerComponentName = new ComponentName(resources.getString(
com.android.internal.R.string.config_defaultDialer),
resources.getString(R.string.incall_default_class));
-
IntentFilter packageIntentFilter = new IntentFilter();
packageIntentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
@@ -176,6 +146,10 @@
UserHandle.USER_ALL);
}
+ public String[] getBTInCallServicePackages() {
+ return mRoleManagerAdapter.getBTInCallService();
+ }
+
public String getDefaultDialerApplication(int userId) {
if (userId == UserHandle.USER_CURRENT) {
userId = ActivityManager.getCurrentUser();
@@ -191,7 +165,7 @@
//
//synchronized (mLock) {
// String defaultDialer = mCurrentDefaultDialerPerUser.get(userId);
- // if (defaultDialer != null) {
+ // if (!TextUtils.isEmpty(defaultDialer)) {
// return defaultDialer;
// }
//}
@@ -237,11 +211,9 @@
public boolean setDefaultDialer(String packageName, int userId) {
boolean isChanged = mDefaultDialerManagerAdapter.setDefaultDialerApplication(
mContext, packageName, userId);
- if(isChanged) {
- synchronized (mLock) {
- // Update the cache synchronously so that there is no delay in cache update.
- mCurrentDefaultDialerPerUser.put(userId, packageName);
- }
+ if (isChanged) {
+ // Update the cache synchronously so that there is no delay in cache update.
+ mCurrentDefaultDialerPerUser.put(userId, packageName == null ? "" : packageName);
}
return isChanged;
}
@@ -249,47 +221,39 @@
private String refreshCacheForUser(int userId) {
String currentDefaultDialer =
mRoleManagerAdapter.getDefaultDialerApp(userId);
- synchronized (mLock) {
- mCurrentDefaultDialerPerUser.put(userId, currentDefaultDialer);
- }
+ mCurrentDefaultDialerPerUser.put(userId, currentDefaultDialer == null ? "" :
+ currentDefaultDialer);
return currentDefaultDialer;
}
/**
* Refreshes the cache for users that currently have packageName as their cached default dialer.
* If packageName is null, refresh all caches.
+ *
* @param packageName Name of the affected package.
*/
private void refreshCachesForUsersWithPackage(String packageName) {
- for (int i = 0; i < mCurrentDefaultDialerPerUser.size(); i++) {
- int userId = mCurrentDefaultDialerPerUser.keyAt(i);
- if (packageName == null ||
- Objects.equals(packageName, mCurrentDefaultDialerPerUser.get(userId))) {
+ mCurrentDefaultDialerPerUser.forEach((userId, currentName) -> {
+ if (packageName == null || Objects.equals(packageName, currentName)) {
String newDefaultDialer = refreshCacheForUser(userId);
Log.v(LOG_TAG, "Refreshing default dialer for user %d: now %s",
userId, newDefaultDialer);
}
- }
+ });
}
public void dumpCache(IndentingPrintWriter pw) {
- synchronized (mLock) {
- for (int i = 0; i < mCurrentDefaultDialerPerUser.size(); i++) {
- pw.printf("User %d: %s\n", mCurrentDefaultDialerPerUser.keyAt(i),
- mCurrentDefaultDialerPerUser.valueAt(i));
- }
- }
+ mCurrentDefaultDialerPerUser.forEach((k, v) -> pw.printf("User %d: %s\n", k, v));
}
private void removeUserFromCache(int userId) {
- synchronized (mLock) {
- mCurrentDefaultDialerPerUser.remove(userId);
- }
+ mCurrentDefaultDialerPerUser.remove(userId);
}
/**
* registerContentObserver is really hard to mock out, so here is a getter method for the
* content observer for testing instead.
+ *
* @return The content observer
*/
@VisibleForTesting
@@ -300,4 +264,30 @@
public RoleManagerAdapter getRoleManagerAdapter() {
return mRoleManagerAdapter;
}
-}
\ No newline at end of file
+
+ public interface DefaultDialerManagerAdapter {
+ String getDefaultDialerApplication(Context context);
+
+ String getDefaultDialerApplication(Context context, int userId);
+
+ boolean setDefaultDialerApplication(Context context, String packageName, int userId);
+ }
+
+ static class DefaultDialerManagerAdapterImpl implements DefaultDialerManagerAdapter {
+ @Override
+ public String getDefaultDialerApplication(Context context) {
+ return DefaultDialerManager.getDefaultDialerApplication(context);
+ }
+
+ @Override
+ public String getDefaultDialerApplication(Context context, int userId) {
+ return DefaultDialerManager.getDefaultDialerApplication(context, userId);
+ }
+
+ @Override
+ public boolean setDefaultDialerApplication(Context context, String packageName,
+ int userId) {
+ return DefaultDialerManager.setDefaultDialerApplication(context, packageName, userId);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
index af79da3..cce8c66 100644
--- a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
+++ b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
@@ -16,7 +16,7 @@
package com.android.server.telecom;
-import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+import static android.telephony.TelephonyManager.EmergencyCallDiagnosticData;
import android.os.BugreportManager;
import android.os.DropBoxManager;
@@ -156,25 +156,26 @@
List<Integer> dataCollectionTypes = getDataCollectionTypes(reason);
boolean invokeTelephonyPersistApi = false;
CallEventTimestamps ts = mEmergencyCallsMap.get(call);
- EmergencyCallDiagnosticParams dp =
- new EmergencyCallDiagnosticParams();
+ EmergencyCallDiagnosticData.Builder callDiagnosticBuilder =
+ new EmergencyCallDiagnosticData.Builder();
for (Integer dataCollectionType : dataCollectionTypes) {
switch (dataCollectionType) {
case COLLECTION_TYPE_TELECOM_STATE:
if (isTelecomDumpCollectionEnabled()) {
- dp.setTelecomDumpSysCollection(true);
+ callDiagnosticBuilder.setTelecomDumpsysCollectionEnabled(true);
invokeTelephonyPersistApi = true;
}
break;
case COLLECTION_TYPE_TELEPHONY_STATE:
if (isTelephonyDumpCollectionEnabled()) {
- dp.setTelephonyDumpSysCollection(true);
+ callDiagnosticBuilder.setTelephonyDumpsysCollectionEnabled(true);
invokeTelephonyPersistApi = true;
}
break;
case COLLECTION_TYPE_LOGCAT_BUFFERS:
if (isLogcatCollectionEnabled()) {
- dp.setLogcatCollection(true, ts.getCallCreatedTime());
+ callDiagnosticBuilder.setLogcatCollectionStartTimeMillis(
+ ts.getCallCreatedTime());
invokeTelephonyPersistApi = true;
}
break;
@@ -191,13 +192,14 @@
default:
}
}
+ EmergencyCallDiagnosticData ecdData = callDiagnosticBuilder.build();
if (invokeTelephonyPersistApi) {
mAsyncTaskExecutor.execute(new Runnable() {
@Override
public void run() {
- Log.i(this, "Requesting Telephony to persist data %s", dp.toString());
+ Log.i(this, "Requesting Telephony to persist data %s", ecdData.toString());
try {
- mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp);
+ mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, ecdData);
} catch (Exception e) {
Log.w(this,
"Exception while invoking "
@@ -337,12 +339,11 @@
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
- if (call != null && mEmergencyCallsMap.get(call) != null && newState == CallState.ACTIVE) {
- CallEventTimestamps ts = mEmergencyCallsMap.get(call);
- if (ts != null) {
- long currentTime = mClockProxy.currentTimeMillis();
- ts.setCallActiveTime(currentTime);
- }
+ CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+ if (call != null && ts != null && newState == CallState.ACTIVE
+ && ts.getCallActiveTime() == 0) {
+ long currentTime = mClockProxy.currentTimeMillis();
+ ts.setCallActiveTime(currentTime);
}
}
@@ -402,7 +403,12 @@
Log.i(this, "skipped dumping diagnostic data");
return;
}
- dumpDiagnosticDataFromDropbox(pw);
+ try {
+ dumpDiagnosticDataFromDropbox(pw);
+ } catch (Exception e) {
+ pw.println("Exception was thrown while dumping diagnostic data from DropBox");
+ e.printStackTrace();
+ }
}
private static class CallEventTimestamps {
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index 5ab0e99..c0e38ca 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -24,6 +24,7 @@
import android.telecom.PhoneAccountHandle;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
/**
* Helps with emergency calls by:
@@ -51,19 +52,25 @@
private long mLastEmergencyCallTimestampMillis;
private long mLastOutgoingEmergencyCallTimestampMillis;
+ private final FeatureFlags mFeatureFlags;
+
@VisibleForTesting
public EmergencyCallHelper(
Context context,
DefaultDialerCache defaultDialerCache,
- Timeouts.Adapter timeoutsAdapter) {
+ Timeouts.Adapter timeoutsAdapter,
+ FeatureFlags featureFlags) {
mContext = context;
mDefaultDialerCache = defaultDialerCache;
mTimeoutsAdapter = timeoutsAdapter;
+ mFeatureFlags = featureFlags;
}
@VisibleForTesting
public void maybeGrantTemporaryLocationPermission(Call call, UserHandle userHandle) {
- if (shouldGrantTemporaryLocationPermission(call)) {
+ if (shouldGrantTemporaryLocationPermission(call) && (
+ !mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()
+ || !wasGrantedTemporaryLocationPermission())) {
grantLocationPermission(userHandle);
}
if (call != null && call.isEmergencyCall()) {
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index 7458f54..afc82ae 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -103,7 +103,7 @@
if ((event != null) && ((event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) ||
(event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))) {
synchronized (mLock) {
- Log.v(this, "SessionCallback: HEADSETHOOK/MEDIA_PLAY_PAUSE");
+ Log.i(this, "onMediaButton: event=%s", event);
boolean consumed = handleCallMediaButton(event);
Log.v(this, "==> handleCallMediaButton(): consumed = %b.", consumed);
return consumed;
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 9ce10bd..8836fff 100755
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -20,6 +20,7 @@
import android.os.Binder;
import android.os.Bundle;
import android.os.ResultReceiver;
+import android.os.UserHandle;
import android.telecom.CallEndpoint;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
@@ -420,7 +421,8 @@
Log.startSession(LogUtils.Sessions.ICA_ENTER_AUDIO_PROCESSING,
mOwnerPackageAbbreviation);
// TODO: enforce the extra permission.
- Binder.withCleanCallingIdentity(() -> {
+ long token = Binder.clearCallingIdentity();
+ try {
synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
@@ -429,7 +431,9 @@
Log.w(this, "enterBackgroundAudioProcessing, unknown call id: %s", callId);
}
}
- });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
} finally {
Log.endSession();
}
@@ -440,7 +444,8 @@
try {
Log.startSession(LogUtils.Sessions.ICA_EXIT_AUDIO_PROCESSING,
mOwnerPackageAbbreviation);
- Binder.withCleanCallingIdentity(() -> {
+ long token = Binder.clearCallingIdentity();
+ try {
synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
@@ -450,7 +455,9 @@
"exitBackgroundAudioProcessing, unknown call id: %s", callId);
}
}
- });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
} finally {
Log.endSession();
}
@@ -599,7 +606,7 @@
synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
- call.sendCallEvent(event, targetSdkVer, extras);
+ call.sendCallEvent(event, extras);
} else {
Log.w(this, "sendCallEvent, unknown call id: %s", callId);
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index d5689ae..3f8f579 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -23,6 +23,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
+import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.AttributionSource;
@@ -44,10 +45,8 @@
import android.os.Looper;
import android.os.PackageTagsList;
import android.os.RemoteException;
-import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
-import android.permission.PermissionManager;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
import android.telecom.ConnectionService;
@@ -59,6 +58,7 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
// TODO: Needed for move to system service: import com.android.internal.R;
@@ -66,11 +66,13 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.SystemStateHelper.SystemStateListener;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.ui.NotificationChannelManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -80,6 +82,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
@@ -99,7 +102,10 @@
UUID.fromString("0c2adf96-353a-433c-afe9-1e5564f304f9");
public static final String SET_IN_CALL_ADAPTER_ERROR_MSG =
"Exception thrown while setting the in-call adapter.";
-
+ public static final UUID NULL_IN_CALL_SERVICE_BINDING_UUID =
+ UUID.fromString("7d58dedf-b71d-4c18-9d23-47b434bde58b");
+ public static final String NULL_IN_CALL_SERVICE_BINDING_ERROR_MSG =
+ "InCallController#sendCallToInCallService with null InCallService binding";
@VisibleForTesting
public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
mAnomalyReporter = mAnomalyReporterAdapter;
@@ -299,7 +305,7 @@
//this is really used for cases where the userhandle for a call
//does not match what we want to use for bindAsUser
- private final UserHandle mUserHandleToUseForBinding;
+ private UserHandle mUserHandleToUseForBinding;
public InCallServiceBindingConnection(InCallServiceInfo info) {
mInCallServiceInfo = info;
@@ -324,8 +330,14 @@
addCall(call);
// Notify this new added call
- sendCallToService(call, mInCallServiceInfo,
- mInCallServices.get(userFromCall).get(mInCallServiceInfo));
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && mInCallServiceInfo.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
+ sendCallToService(call, mInCallServiceInfo, mBTInCallServices
+ .get(userFromCall).second);
+ } else {
+ sendCallToService(call, mInCallServiceInfo,
+ mInCallServices.get(userFromCall).get(mInCallServiceInfo));
+ }
}
return CONNECTION_SUCCEEDED;
}
@@ -351,7 +363,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
@@ -359,7 +372,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.
@@ -374,6 +388,8 @@
+ "INTERACT_ACROSS_USERS permission");
}
}
+ // Used for referencing what user we used to bind to the given ICS.
+ mUserHandleToUseForBinding = userToBind;
Log.i(this, "using user id: %s for binding. User from Call is: %s", userToBind,
userFromCall);
if (!mContext.bindServiceAsUser(intent, mServiceConnection,
@@ -1069,6 +1085,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());
@@ -1098,12 +1115,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,
@@ -1116,11 +1157,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);
}
@@ -1173,6 +1214,11 @@
private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
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 };
@@ -1180,8 +1226,13 @@
/** The in-call app implementations, see {@link IInCallService}. */
private final Map<UserHandle, Map<InCallServiceInfo, IInCallService>>
mInCallServices = new ArrayMap<>();
+ private final Map<UserHandle, Pair<InCallServiceInfo, IInCallService>> mBTInCallServices =
+ new ArrayMap<>();
+ private final Map<UserHandle, Map<InCallServiceInfo, IInCallService>>
+ mCombinedInCallServiceMap = new ArrayMap<>();
private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
+ private final Collection<Call> mBtIcsCallTracker = new ArraySet<>();
private final Context mContext;
private final AppOpsManager mAppOpsManager;
@@ -1197,8 +1248,11 @@
mInCallServiceConnections = new ArrayMap<>();
private final Map<UserHandle, NonUIInCallServiceConnectionCollection>
mNonUIInCallServiceConnections = new ArrayMap<>();
+ private final Map<UserHandle, InCallServiceBindingConnection> mBTInCallServiceConnections =
+ new ArrayMap<>();
private final ClockProxy mClockProxy;
private final IBinder mToken = new Binder();
+ private final FeatureFlags mFeatureFlags;
// A set of known non-UI in call services on the device, including those that are disabled.
// We track this so that we can efficiently bind to them when we're notified that a new
@@ -1209,6 +1263,15 @@
// The future will complete with true if binding succeeds, false if it timed out.
private CompletableFuture<Boolean> mBindingFuture = CompletableFuture.completedFuture(true);
+ // Future that's in a completed state unless we're in the middle of a binding to a bluetooth
+ // in-call service.
+ // The future will complete with true if bluetooth in-call service succeeds, false if it timed
+ // out.
+ 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;
/**
@@ -1230,6 +1293,8 @@
private boolean mIsStartCallDelayScheduled = false;
+ private boolean mDisconnectedToneStartedPlaying = false;
+
/**
* A list of call IDs which are currently using the camera.
*/
@@ -1238,10 +1303,22 @@
private ArraySet<String> mAllCarrierPrivilegedApps = new ArraySet<>();
private ArraySet<String> mActiveCarrierPrivilegedApps = new ArraySet<>();
+ private java.lang.Runnable mCallRemovedRunnable;
+
public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
- CarModeTracker carModeTracker, ClockProxy clockProxy) {
+ CarModeTracker carModeTracker, ClockProxy clockProxy, FeatureFlags featureFlags) {
+ this(context, lock, callsManager, systemStateHelper, defaultDialerCache, timeoutsAdapter,
+ emergencyCallHelper, carModeTracker, clockProxy, featureFlags, null);
+ }
+
+ @VisibleForTesting
+ public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
+ SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
+ Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
+ CarModeTracker carModeTracker, ClockProxy clockProxy, FeatureFlags featureFlags,
+ com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags) {
mContext = context;
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
@@ -1258,6 +1335,7 @@
IntentFilter userAddedFilter = new IntentFilter(Intent.ACTION_USER_ADDED);
userAddedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiver(mUserAddedReceiver, userAddedFilter);
+ mFeatureFlags = featureFlags;
}
private void restrictPhoneCallOps() {
@@ -1337,87 +1415,134 @@
// Track the call if we don't already know about it.
addCall(call);
- if (!isBoundAndConnectedToServices(userFromCall)) {
- Log.i(this, "onCallAdded: %s; not bound or connected.", call);
- // We are not bound, or we're not connected.
- bindToServices(call);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ boolean bindingToBtRequired = false;
+ boolean bindingToOtherServicesRequired = false;
+ if (!isBoundAndConnectedToBTService(userFromCall)) {
+ Log.i(this, "onCallAdded: %s; not bound or connected to BT ICS.", 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.
+ bindingToOtherServicesRequired = true;
+ bindToServices(call);
+ }
+ // 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 {
- InCallServiceConnection inCallServiceConnection =
- mInCallServiceConnections.get(userFromCall);
-
- // We are bound, and we are connected.
- adjustServiceBindingsForEmergency(userFromCall);
-
- // This is in case an emergency call is added while there is an existing call.
- mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
- userFromCall);
-
- if (inCallServiceConnection != null) {
- Log.i(this, "mInCallServiceConnection isConnected=%b",
- inCallServiceConnection.isConnected());
+ if (!isBoundAndConnectedToServices(userFromCall)) {
+ Log.i(this, "onCallAdded: %s; not bound or connected.", call);
+ // We are not bound, or we're not connected.
+ bindToServices(call);
+ } else {
+ addCallToConnectedServices(call, userFromCall);
}
+ }
+ }
- List<ComponentName> componentsUpdated = new ArrayList<>();
- if (mInCallServices.containsKey(userFromCall)) {
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
- get(userFromCall).entrySet()) {
- InCallServiceInfo info = entry.getKey();
+ private void addCallToConnectedServices(Call call, UserHandle userFromCall) {
+ InCallServiceConnection inCallServiceConnection =
+ mInCallServiceConnections.get(userFromCall);
- if (call.isExternalCall() && !info.isExternalCallsSupported()) {
- continue;
- }
+ // We are bound, and we are connected.
+ adjustServiceBindingsForEmergency(userFromCall);
- if (call.isSelfManaged() && (!call.visibleToInCallService()
- || !info.isSelfManagedCallsSupported())) {
- continue;
- }
+ // This is in case an emergency call is added while there is an existing call.
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call, userFromCall);
- // Only send the RTT call if it's a UI in-call service
- boolean includeRttCall = false;
- if (inCallServiceConnection != null) {
- includeRttCall = info.equals(inCallServiceConnection.getInfo());
- }
+ if (inCallServiceConnection != null) {
+ Log.i(this, "mInCallServiceConnection isConnected=%b",
+ inCallServiceConnection.isConnected());
+ }
- componentsUpdated.add(info.getComponentName());
- IInCallService inCallService = entry.getValue();
+ List<ComponentName> componentsUpdated = new ArrayList<>();
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry :
+ serviceMap.get(userFromCall).entrySet()) {
+ InCallServiceInfo info = entry.getKey();
- ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
- true /* includeVideoProvider */,
- mCallsManager.getPhoneAccountRegistrar(),
- info.isExternalCallsSupported(), includeRttCall,
- info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
- info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
- try {
- inCallService.addCall(
- sanitizeParcelableCallForService(info, parcelableCall));
- updateCallTracking(call, info, true /* isAdd */);
- } catch (RemoteException ignored) {
- }
+ if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+ continue;
}
- Log.i(this, "Call added to components: %s", componentsUpdated);
+
+ if (call.isSelfManaged() && (!call.visibleToInCallService()
+ || !info.isSelfManagedCallsSupported())) {
+ continue;
+ }
+
+ // Only send the RTT call if it's a UI in-call service
+ boolean includeRttCall = false;
+ if (inCallServiceConnection != null) {
+ includeRttCall = info.equals(inCallServiceConnection.getInfo());
+ }
+
+ componentsUpdated.add(info.getComponentName());
+ IInCallService inCallService = entry.getValue();
+
+ ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
+ true /* includeVideoProvider */,
+ mCallsManager.getPhoneAccountRegistrar(),
+ info.isExternalCallsSupported(), includeRttCall,
+ info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
+ info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
+ try {
+ inCallService.addCall(
+ sanitizeParcelableCallForService(info, parcelableCall));
+ updateCallTracking(call, info, true /* isAdd */);
+ } catch (RemoteException ignored) {
+ }
}
+ Log.i(this, "Call added to ICS: %s", componentsUpdated);
}
}
@Override
public void onCallRemoved(Call call) {
Log.i(this, "onCallRemoved: %s", call);
- if (mCallsManager.getCalls().isEmpty()) {
+ // Instead of checking if there are no active calls, we should check if there any calls with
+ // the same associated user returned from getUserFromCall. For instance, it's possible to
+ // have calls coexist on the personal profile and work profile, in which case, we would only
+ // remove the ICS connection for the user associated with the call to be disconnected.
+ UserHandle userFromCall = getUserFromCall(call);
+ Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream()
+ .filter((c) -> getUserFromCall(c).equals(userFromCall));
+ boolean isCallCountZero = mFeatureFlags.associatedUserRefactorForWorkProfile()
+ ? callsAssociatedWithUserFromCall.count() == 0
+ : mCallsManager.getCalls().isEmpty();
+ if (isCallCountZero) {
/** Let's add a 2 second delay before we send unbind to the services to hopefully
* give them enough time to process all the pending messages.
*/
- mHandler.postDelayed(new Runnable("ICC.oCR", mLock) {
+ if (mCallRemovedRunnable != null
+ && mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()) {
+ mHandler.removeCallbacks(mCallRemovedRunnable);
+ }
+ mCallRemovedRunnable = new Runnable("ICC.oCR", mLock) {
@Override
public void loggedRun() {
- // Check again to make sure there are no active calls.
- if (mCallsManager.getCalls().isEmpty()) {
- unbindFromServices(getUserFromCall(call));
-
+ // Check again to make sure there are no active calls for the associated user.
+ Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream()
+ .filter((c) -> getUserFromCall(c).equals(userFromCall));
+ boolean isCallCountZero = mFeatureFlags.associatedUserRefactorForWorkProfile()
+ ? callsAssociatedWithUserFromCall.count() == 0
+ : mCallsManager.getCalls().isEmpty();
+ if (isCallCountZero) {
+ unbindFromServices(userFromCall);
mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
}
}
- }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
- mContext.getContentResolver()));
+ }.prepare();
+ mHandler.postDelayed(mCallRemovedRunnable,
+ mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+ mContext.getContentResolver()));
}
call.removeListener(mCallListener);
mCallIdMapper.removeCall(call);
@@ -1430,15 +1555,110 @@
}
@Override
+ public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
+ Log.i(this, "onDisconnectedTonePlaying: %s -> %b", call, isTonePlaying);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ synchronized (mLock) {
+ 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);
+ }
+ // Schedule unbinding of BT ICS.
+ maybeScheduleBtUnbind(call);
+ }
+ }
+ }
+ }
+
+ public void maybeScheduleBtUnbind(Call call) {
+ mBtIcsCallTracker.remove(call);
+ // Track the current calls that are being tracked by the BT ICS and determine the
+ // associated users of those calls as well as the users which have been used to bind to the
+ // ICS.
+ Set<UserHandle> usersFromOngoingCalls = new ArraySet<>();
+ Set<UserHandle> usersCurrentlyBound = new ArraySet<>();
+ for (Call pendingCall : mBtIcsCallTracker) {
+ UserHandle userFromPendingCall = getUserFromCall(pendingCall);
+ final InCallServiceBindingConnection pendingCallConnection =
+ mBTInCallServiceConnections.get(userFromPendingCall);
+ usersFromOngoingCalls.add(userFromPendingCall);
+ if (pendingCallConnection != null) {
+ usersCurrentlyBound.add(pendingCallConnection.mUserHandleToUseForBinding);
+ }
+ }
+
+ UserHandle userHandle = getUserFromCall(call);
+ // Refrain from unbinding ICS and clearing the ICS mapping if there's an ongoing call under
+ // the same associated user. Make sure we keep the internal mappings so that they aren't
+ // cleared until that call is disconnected. Note here that if the associated users are the
+ // same, the user used for the binding will also be the same.
+ if (usersFromOngoingCalls.contains(userHandle)) {
+ Log.i(this, "scheduleBtUnbind: Refraining from unbinding BT service due to an ongoing "
+ + "call detected under the same user (%s).", userHandle);
+ return;
+ }
+
+ if (mBTInCallServiceConnections.containsKey(userHandle)) {
+ Log.i(this, "scheduleBtUnbind: Schedule unbind BT service");
+ final InCallServiceBindingConnection connection =
+ mBTInCallServiceConnections.get(userHandle);
+ // The user that was used for binding may be different than the user from call
+ // (associated user), which is what we use to reference the BT ICS bindings. For
+ // example, consider the work profile scenario where the BT ICS is only available under
+ // User 0: in this case, the user to bind to will be User 0 whereas we store the
+ // references to this connection and BT ICS under the work user. This logic ensures
+ // that we prevent unbinding the BT ICS if there is a personal (associatedUser: 0) call
+ // + work call (associatedUser: 10) and one of them gets disconnected.
+ if (usersCurrentlyBound.contains(connection.mUserHandleToUseForBinding)) {
+ Log.i(this, "scheduleBtUnbind: Refraining from unbinding BT service to an "
+ + "ongoing call detected which is bound to the same user (%s).",
+ connection.mUserHandleToUseForBinding);
+ } else {
+ // Similar to in onCallRemoved when we unbind from the other ICS, we need to
+ // delay unbinding from the BT ICS because we need to give the ICS a
+ // moment to finish the onCallRemoved signal it got just prior.
+ mHandler.postDelayed(new Runnable("ICC.sBU", mLock) {
+ @Override
+ public void loggedRun() {
+ Log.i(this, "onDisconnectedTonePlaying: unbinding from BT ICS.");
+ // Prevent unbinding in the case that this is run while another call
+ // has been placed/received. Otherwise, we will early unbind from
+ // the BT ICS and not be able to properly relay call state updates.
+ if (!mBTInCallServiceConnections.containsKey(userHandle)) {
+ connection.disconnect();
+ } else {
+ Log.i(this, "onDisconnectedTonePlaying: Refraining from "
+ + "unbinding BT ICS. Another call is ongoing.");
+ }
+ }
+ }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+ mContext.getContentResolver()));
+ }
+ mBTInCallServiceConnections.remove(userHandle);
+ }
+ // Ensure that BT ICS instance is cleaned up
+ if (mBTInCallServices.remove(userHandle) != null) {
+ updateCombinedInCallServiceMap(userHandle);
+ }
+ }
+
+ @Override
public void onExternalCallChanged(Call call, boolean isExternalCall) {
Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
List<ComponentName> componentsUpdated = new ArrayList<>();
UserHandle userFromCall = getUserFromCall(call);
- if (!isExternalCall && mInCallServices.containsKey(userFromCall)) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (!isExternalCall && serviceMap.containsKey(userFromCall)) {
// The call was external but it is no longer external. We must now add it to any
// InCallServices which do not support external calls.
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry : serviceMap.
get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
@@ -1477,9 +1697,9 @@
// InCallServices which do not support external calls.
// Remove the call by sending a call update indicating the call was disconnected.
Log.i(this, "Removing external call %s", call);
- if (mInCallServices.containsKey(userFromCall)) {
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
- get(userFromCall).entrySet()) {
+ if (serviceMap.containsKey(userFromCall)) {
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry :
+ serviceMap.get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
if (info.isExternalCallsSupported()) {
// For InCallServices which support external calls, we do not need to remove
@@ -1515,6 +1735,8 @@
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
+ Log.i(this, "onCallStateChanged: Call state changed for TC@%s: %s -> %s", call.getId(),
+ CallState.toString(oldState), CallState.toString(newState));
maybeTrackMicrophoneUse(isMuted());
updateCall(call);
}
@@ -1530,11 +1752,13 @@
@Override
public void onCallAudioStateChanged(CallAudioState oldCallAudioState,
CallAudioState newCallAudioState) {
- if (!mInCallServices.isEmpty()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (!serviceMap.isEmpty()) {
Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
newCallAudioState);
maybeTrackMicrophoneUse(newCallAudioState.isMuted());
- mInCallServices.values().forEach(inCallServices -> {
+ serviceMap.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onCallAudioStateChanged(newCallAudioState);
@@ -1547,9 +1771,11 @@
@Override
public void onCallEndpointChanged(CallEndpoint callEndpoint) {
- if (!mInCallServices.isEmpty()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (!serviceMap.isEmpty()) {
Log.i(this, "Calling onCallEndpointChanged");
- mInCallServices.values().forEach(inCallServices -> {
+ serviceMap.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onCallEndpointChanged(callEndpoint);
@@ -1563,10 +1789,12 @@
@Override
public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
- if (!mInCallServices.isEmpty()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (!serviceMap.isEmpty()) {
Log.i(this, "Calling onAvailableCallEndpointsChanged");
List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
- mInCallServices.values().forEach(inCallServices -> {
+ serviceMap.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onAvailableCallEndpointsChanged(availableEndpoints);
@@ -1580,9 +1808,11 @@
@Override
public void onMuteStateChanged(boolean isMuted) {
- if (!mInCallServices.isEmpty()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (!serviceMap.isEmpty()) {
Log.i(this, "Calling onMuteStateChanged");
- mInCallServices.values().forEach(inCallServices -> {
+ serviceMap.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onMuteStateChanged(isMuted);
@@ -1596,9 +1826,11 @@
@Override
public void onCanAddCallChanged(boolean canAddCall) {
- if (!mInCallServices.isEmpty()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (!serviceMap.isEmpty()) {
Log.i(this, "onCanAddCallChanged : %b", canAddCall);
- mInCallServices.values().forEach(inCallServices -> {
+ serviceMap.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onCanAddCallChanged(canAddCall);
@@ -1611,9 +1843,11 @@
void onPostDialWait(Call call, String remaining) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
- for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+ for (IInCallService inCallService: serviceMap.get(userFromCall).values()) {
try {
inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
} catch (RemoteException ignored) {
@@ -1688,10 +1922,34 @@
}
}
- @VisibleForTesting
public void bringToForeground(boolean showDialpad, UserHandle callingUser) {
- if (mInCallServices.containsKey(callingUser)) {
- for (IInCallService inCallService : mInCallServices.get(callingUser).values()) {
+ KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
+ boolean isLockscreenRestricted = keyguardManager != null
+ && keyguardManager.isKeyguardLocked();
+ UserHandle currentUser = mCallsManager.getCurrentUserHandle();
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ // Handle cases when calls are placed from the keyguard UI screen, which operates under
+ // the admin user. This needs to account for emergency calls placed from secondary/guest
+ // users as well as the work profile. Once the screen is locked, the user should be able to
+ // return to the call (from the keyguard UI).
+ if (mFeatureFlags.eccKeyguard() && mCallsManager.isInEmergencyCall()
+ && isLockscreenRestricted && !serviceMap.containsKey(callingUser)) {
+ // If screen is locked and the current user is the system, query calls for the work
+ // profile user, if available. Otherwise, the user is in the secondary/guest profile,
+ // so we can default to the system user.
+ if (currentUser.isSystem()) {
+ UserManager um = mContext.getSystemService(UserManager.class);
+ UserHandle workProfileUser = findChildManagedProfileUser(currentUser, um);
+ boolean hasWorkCalls = mCallsManager.getCalls().stream()
+ .filter((c) -> getUserFromCall(c).equals(workProfileUser)).count() > 0;
+ callingUser = hasWorkCalls ? workProfileUser : currentUser;
+ } else {
+ callingUser = currentUser;
+ }
+ }
+ if (serviceMap.containsKey(callingUser)) {
+ for (IInCallService inCallService : serviceMap.get(callingUser).values()) {
try {
inCallService.bringToForeground(showDialpad);
} catch (RemoteException ignored) {
@@ -1704,7 +1962,7 @@
@VisibleForTesting
public Map<UserHandle, Map<InCallServiceInfo, IInCallService>> getInCallServices() {
- return mInCallServices;
+ return getCombinedInCallServiceMap();
}
@VisibleForTesting
@@ -1713,9 +1971,11 @@
}
void silenceRinger(Set<UserHandle> userHandles) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
userHandles.forEach(userHandle -> {
- if (mInCallServices.containsKey(userHandle)) {
- for (IInCallService inCallService : mInCallServices.get(userHandle).values()) {
+ if (serviceMap.containsKey(userHandle)) {
+ for (IInCallService inCallService : serviceMap.get(userHandle).values()) {
try {
inCallService.silenceRinger();
} catch (RemoteException ignored) {
@@ -1727,8 +1987,10 @@
private void notifyConnectionEvent(Call call, String event, Bundle extras) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
- for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
+ for (IInCallService inCallService : serviceMap.get(userFromCall).values()) {
try {
Log.i(this, "notifyConnectionEvent {Call: %s, Event: %s, Extras:[%s]}",
(call != null ? call.toString() : "null"),
@@ -1743,8 +2005,10 @@
private void notifyRttInitiationFailure(Call call, int reason) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
- mInCallServices.get(userFromCall).entrySet().stream()
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
+ serviceMap.get(userFromCall).entrySet().stream()
.filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
get(userFromCall).getInfo()))
.forEach((entry) -> {
@@ -1761,8 +2025,10 @@
private void notifyRemoteRttRequest(Call call, int requestId) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
- mInCallServices.get(userFromCall).entrySet().stream()
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
+ serviceMap.get(userFromCall).entrySet().stream()
.filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
get(userFromCall).getInfo()))
.forEach((entry) -> {
@@ -1779,8 +2045,10 @@
private void notifyHandoverFailed(Call call, int error) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
- for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
+ for (IInCallService inCallService : serviceMap.get(userFromCall).values()) {
try {
inCallService.onHandoverFailed(mCallIdMapper.getCallId(call), error);
} catch (RemoteException ignored) {
@@ -1791,8 +2059,10 @@
private void notifyHandoverComplete(Call call) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
- for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
+ for (IInCallService inCallService : serviceMap.get(userFromCall).values()) {
try {
inCallService.onHandoverComplete(mCallIdMapper.getCallId(call));
} catch (RemoteException ignored) {
@@ -1805,6 +2075,7 @@
* Unbinds an existing bound connection to the in-call app.
*/
public void unbindFromServices(UserHandle userHandle) {
+ Log.i(this, "Unbinding from services for user %s", userHandle);
try {
mContext.unregisterReceiver(mPackageChangedReceiver);
} catch (IllegalArgumentException e) {
@@ -1819,26 +2090,84 @@
mNonUIInCallServiceConnections.get(userHandle).disconnect();
mNonUIInCallServiceConnections.remove(userHandle);
}
- mInCallServices.remove(userHandle);
+ 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);
+ }
+ }
+
+ /**
+ * Binds to Bluetooth InCallServices. Method-invoker must check
+ * {@link #isBoundAndConnectedToBTService(UserHandle)} before invoking.
+ *
+ * @param call The newly added call that triggered the binding to the in-call services.
+ */
+ 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);
+ List<InCallServiceInfo> infos = getInCallServiceComponents(userToBind,
+ IN_CALL_SERVICE_TYPE_BLUETOOTH);
+ boolean serviceUnavailableForUser = false;
+ if (infos.size() == 0 || infos.get(0) == null) {
+ 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.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);
}
/**
* Binds to all the UI-providing InCallService as well as system-implemented non-UI
- * InCallServices. Method-invoker must check {@link #isBoundAndConnectedToServices()}
+ * 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.
+ * @param call The newly added call that triggered the binding to the in-call
+ * services.
*/
@VisibleForTesting
public void bindToServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
- UserHandle parentUser = null;
UserManager um = mContext.getSystemService(UserManager.class);
-
- if (um.isManagedProfile(userFromCall.getIdentifier())) {
+ UserHandle parentUser = mFeatureFlags.profileUserSupport()
+ ? um.getProfileParent(userFromCall) : null;
+ if (!mFeatureFlags.profileUserSupport()
+ && um.isManagedProfile(userFromCall.getIdentifier())) {
parentUser = um.getProfileParent(userFromCall);
- Log.i(this, "child:%s parent:%s", userFromCall, parentUser);
}
+ Log.i(this, "child:%s parent:%s", userFromCall, parentUser);
if (!mInCallServiceConnections.containsKey(userFromCall)) {
InCallServiceConnection dialerInCall = null;
@@ -1889,7 +2218,8 @@
// Actually try binding to the UI InCallService.
if (inCallServiceConnection.connect(call) ==
- InCallServiceConnection.CONNECTION_SUCCEEDED || call.isSelfManaged()) {
+ InCallServiceConnection.CONNECTION_SUCCEEDED || (call != null
+ && call.isSelfManaged())) {
// 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.
@@ -1910,19 +2240,20 @@
private void updateNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
- UserHandle parentUser = null;
UserManager um = mContext.getSystemService(UserManager.class);
- if(um.isManagedProfile(userFromCall.getIdentifier()))
- {
+ UserHandle parentUser = mFeatureFlags.profileUserSupport()
+ ? um.getProfileParent(userFromCall) : null;
+
+ if (!mFeatureFlags.profileUserSupport()
+ && um.isManagedProfile(userFromCall.getIdentifier())) {
parentUser = um.getProfileParent(userFromCall);
}
List<InCallServiceInfo> nonUIInCallComponents =
getInCallServiceComponents(userFromCall, IN_CALL_SERVICE_TYPE_NON_UI);
List<InCallServiceInfo> nonUIInCallComponentsForParent = new ArrayList<>();
- if(parentUser != null)
- {
+ if(parentUser != null) {
//also get Non-UI services using parent handle.
nonUIInCallComponentsForParent =
getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_NON_UI);
@@ -2121,7 +2452,7 @@
ComponentName foundComponentName =
new ComponentName(serviceInfo.packageName, serviceInfo.name);
- if (requestedType == IN_CALL_SERVICE_TYPE_NON_UI) {
+ if (currentType == IN_CALL_SERVICE_TYPE_NON_UI) {
mKnownNonUiInCallServices.add(foundComponentName);
}
@@ -2234,6 +2565,12 @@
return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
}
+ boolean processingBluetoothPackage = isBluetoothPackage(serviceInfo.packageName);
+ if (mFeatureFlags.separatelyBindToBtIncallService() && processingBluetoothPackage
+ && (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
+ return IN_CALL_SERVICE_TYPE_BLUETOOTH;
+ }
+
// Also allow any in-call service that has the control-experience permission (to ensure
// that it is a system app) and doesn't claim to show any UI.
if (!isUIService && !isCarModeUIService && (hasControlInCallPermission ||
@@ -2274,9 +2611,26 @@
trackCallingUserInterfaceStarted(info);
}
IInCallService inCallService = IInCallService.Stub.asInterface(service);
- mInCallServices.putIfAbsent(userHandle,
- new ArrayMap<InCallController.InCallServiceInfo, IInCallService>());
- mInCallServices.get(userHandle).put(info, inCallService);
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
+ 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.get(userHandle).complete(true);
+ }
+ mBTInCallServices.put(userHandle, new Pair<>(info, inCallService));
+ } else {
+ mInCallServices.putIfAbsent(userHandle, new ArrayMap<>());
+ mInCallServices.get(userHandle).put(info, inCallService);
+ }
+
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ updateCombinedInCallServiceMap(userHandle);
+ }
+
try {
inCallService.setInCallAdapter(
new InCallAdapter(
@@ -2288,12 +2642,13 @@
Log.e(this, e, "Failed to set the in-call adapter.");
mAnomalyReporter.reportAnomaly(SET_IN_CALL_ADAPTER_ERROR_UUID,
SET_IN_CALL_ADAPTER_ERROR_MSG);
- Trace.endSection();
return false;
}
// Upon successful connection, send the state of the world to the service.
- List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls());
+ List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls().stream().filter(
+ call -> getUserFromCall(call).equals(userHandle))
+ .collect(Collectors.toUnmodifiableList()));
Log.i(this, "Adding %s calls to InCallService after onConnected: %s, including external " +
"calls", calls.size(), info.getComponentName());
int numCallsSent = 0;
@@ -2303,6 +2658,10 @@
try {
inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
+ if (mFeatureFlags.onCallEndpointChangedIcsOnConnected()) {
+ inCallService.onCallEndpointChanged(mCallsManager.getCallEndpointController()
+ .getCurrentCallEndpoint());
+ }
} catch (RemoteException ignored) {
}
// Don't complete the binding future for non-ui incalls
@@ -2314,7 +2673,8 @@
return true;
}
- private int sendCallToService(Call call, InCallServiceInfo info,
+ @VisibleForTesting
+ public int sendCallToService(Call call, InCallServiceInfo info,
IInCallService inCallService) {
try {
if ((call.isSelfManaged() && (!info.isSelfManagedCallsSupported()
@@ -2340,7 +2700,20 @@
includeRttCall,
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
- inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
+ if (mFeatureFlags.doNotSendCallToNullIcs()) {
+ if (inCallService != null) {
+ inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
+ } else {
+ Log.w(this, "call=[%s], was not sent to InCallService"
+ + " with info=[%s] due to a null InCallService binding",
+ call, info);
+ mAnomalyReporter.reportAnomaly(NULL_IN_CALL_SERVICE_BINDING_UUID,
+ NULL_IN_CALL_SERVICE_BINDING_ERROR_MSG);
+ return 0;
+ }
+ } else {
+ inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
+ }
updateCallTracking(call, info, true /* isAdd */);
return 1;
} catch (RemoteException ignored) {
@@ -2363,6 +2736,11 @@
if (mInCallServices.containsKey(userHandle)) {
mInCallServices.get(userHandle).remove(disconnectedInfo);
}
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
+ mBTInCallServices.remove(userHandle);
+ updateCombinedInCallServiceMap(userHandle);
+ }
}
/**
@@ -2383,17 +2761,19 @@
* @param rttInfoChanged {@code true} if any information about the RTT session changed,
* {@code false} otherwise.
* @param exceptPackageName When specified, this package name will not get a call update.
- * Used ONLY from {@link Call#putConnectionServiceExtras(int, Bundle, String)} to
+ * Used ONLY from {@link Call#putConnectionServiceExtras(Bundle)} to
* ensure we can propagate extras changes between InCallServices but
* not inform the requestor of their own change.
*/
private void updateCall(Call call, boolean videoProviderChanged, boolean rttInfoChanged,
String exceptPackageName) {
UserHandle userFromCall = getUserFromCall(call);
- if (mInCallServices.containsKey(userFromCall)) {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ if (serviceMap.containsKey(userFromCall)) {
Log.i(this, "Sending updateCall %s", call);
List<ComponentName> componentsUpdated = new ArrayList<>();
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry : serviceMap.
get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
ComponentName componentName = info.getComponentName();
@@ -2425,15 +2805,59 @@
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
IInCallService inCallService = entry.getValue();
- componentsUpdated.add(componentName);
+ boolean isDisconnectingBtIcs = info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
+ && call.getState() == CallState.DISCONNECTED;
- try {
- inCallService.updateCall(
- sanitizeParcelableCallForService(info, parcelableCall));
- } catch (RemoteException ignored) {
+ if (isDisconnectingBtIcs) {
+ // If this is the first we heard about the disconnect for the BT ICS, then we
+ // will setup a future to notify the disconnet later.
+ if (!mDisconnectedToneBtFutures.containsKey(call.getId())) {
+ // Create the base future with timeout, we will chain more operations on to
+ // this.
+ CompletableFuture<Void> disconnectedToneFuture =
+ new CompletableFuture<Void>()
+ .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ // Note: DO NOT chain async work onto this future; using thenRun ensures
+ // when disconnectedToneFuture is completed that the chained work is run
+ // synchronously.
+ disconnectedToneFuture.thenRun(() -> {
+ Log.i(this,
+ "updateCall: (deferred) Sending call disconnected update "
+ + "to BT ICS.");
+ updateCallToIcs(inCallService, info, parcelableCall, componentName);
+ synchronized (mLock) {
+ mDisconnectedToneBtFutures.remove(call.getId());
+ }
+ });
+ mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
+ } else {
+ // If we have already cached a disconnect signal for the BT ICS, don't sent
+ // any other updates (ie due to extras or whatnot) to the BT ICS. If we do
+ // then it will hear about the disconnect in advance and not play the call
+ // end tone.
+ Log.i(this, "updateCall: skip update for disconnected call to BT ICS");
+ }
+ } else {
+ componentsUpdated.add(componentName);
+ updateCallToIcs(inCallService, info, parcelableCall, componentName);
}
}
Log.i(this, "Components updated: %s", componentsUpdated);
+ } else {
+ Log.i(this,
+ "Unable to update call. InCallService not found for user: %s", userFromCall);
+ }
+ }
+
+ 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);
}
}
@@ -2443,6 +2867,10 @@
*/
@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);
@@ -2453,6 +2881,9 @@
if (mCallIdMapper.getCallId(call) == null) {
mCallIdMapper.addCall(call);
call.addListener(mCallListener);
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ mBtIcsCallTracker.add(call);
+ }
}
maybeTrackMicrophoneUse(isMuted());
@@ -2468,6 +2899,14 @@
return mInCallServiceConnections.get(userHandle).isConnected();
}
+ @VisibleForTesting
+ public boolean isBoundAndConnectedToBTService(UserHandle userHandle) {
+ if (!mBTInCallServiceConnections.containsKey(userHandle)) {
+ return false;
+ }
+ return mBTInCallServiceConnections.get(userHandle).isConnected();
+ }
+
/**
* @return A future that is pending whenever we are in the middle of binding to an
* incall service.
@@ -2477,14 +2916,33 @@
}
/**
+ * @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.
*/
public void dump(IndentingPrintWriter pw) {
- pw.println("mInCallServices (InCalls registered):");
+ pw.println("combinedInCallServiceMap (InCalls registered):");
pw.increaseIndent();
- mInCallServices.values().forEach(inCallServices -> {
+ Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+ getCombinedInCallServiceMap();
+ serviceMap.values().forEach(inCallServices -> {
for (InCallServiceInfo info : inCallServices.keySet()) {
pw.println(info);
}
@@ -2779,19 +3237,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.
*/
@@ -2871,18 +3348,91 @@
return mCallsManager.getCurrentUserHandle();
} else {
UserHandle userFromCall = call.getAssociatedUser();
- UserManager userManager = mContext.getSystemService(UserManager.class);
- // Emergency call should never be blocked, so if the user associated with call is in
- // quite mode, use the primary user for the emergency call.
+ 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
+ // sims (i.e. work sim), where the associated user would be the target phone account
+ // handle user.
if ((call.isEmergencyCall() || call.isInECBM())
&& (userManager.isQuietModeEnabled(userFromCall)
// We should also account for secondary/guest users where the profile may not
// necessarily be paused.
- || !userManager.isUserAdmin(mCallsManager.getCurrentUserHandle()
- .getIdentifier()))) {
+ || !isCurrentUserAdmin)) {
return mCallsManager.getCurrentUserHandle();
}
return userFromCall;
}
}
+
+ /**
+ * Useful for debugging purposes and called on the command line via
+ * an "adb shell telecom command".
+ *
+ * @return true if a particular non-ui InCallService package is bound in a call.
+ */
+ public boolean isNonUiInCallServiceBound(String packageName) {
+ for (NonUIInCallServiceConnectionCollection ics : mNonUIInCallServiceConnections.values()) {
+ for (InCallServiceBindingConnection connection : ics.getSubConnections()) {
+ InCallServiceInfo serviceInfo = connection.mInCallServiceInfo;
+ Log.i(this, "isNonUiInCallServiceBound: found serviceInfo=[%s]", serviceInfo);
+ if (serviceInfo != null &&
+ serviceInfo.mComponentName.getPackageName().contains(packageName)) {
+ Log.i(this, "isNonUiInCallServiceBound: found target package");
+ return true;
+ }
+ }
+ }
+ // 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) {
+ synchronized (mLock) {
+ Map<InCallServiceInfo, IInCallService> serviceMap;
+ if (mInCallServices.containsKey(user)) {
+ serviceMap = mInCallServices.get(user);
+ } else {
+ serviceMap = new HashMap<>();
+ }
+ if (mFeatureFlags.separatelyBindToBtIncallService()
+ && mBTInCallServices.containsKey(user)) {
+ Pair<InCallServiceInfo, IInCallService> btServicePair = mBTInCallServices.get(user);
+ serviceMap.put(btServicePair.first, btServicePair.second);
+ }
+ if (!serviceMap.isEmpty()) {
+ mCombinedInCallServiceMap.put(user, serviceMap);
+ } else {
+ mCombinedInCallServiceMap.remove(user);
+ }
+ }
+ }
+
+ private Map<UserHandle,
+ Map<InCallController.InCallServiceInfo, IInCallService>> getCombinedInCallServiceMap() {
+ synchronized (mLock) {
+ if (mFeatureFlags.separatelyBindToBtIncallService()) {
+ return mCombinedInCallServiceMap;
+ } else {
+ return mInCallServices;
+ }
+ }
+ }
+
+ 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/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index 3cc4aac..b7edeb5 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -30,6 +30,7 @@
import android.telecom.Logging.Session;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -54,25 +55,28 @@
private final ToneGeneratorFactory mToneGeneratorFactory;
private final MediaPlayerFactory mMediaPlayerFactory;
private final AudioManagerAdapter mAudioManagerAdapter;
+ private final FeatureFlags mFeatureFlags;
public Factory(CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
TelecomSystem.SyncRoot lock, ToneGeneratorFactory toneGeneratorFactory,
- MediaPlayerFactory mediaPlayerFactory, AudioManagerAdapter audioManagerAdapter) {
+ MediaPlayerFactory mediaPlayerFactory, AudioManagerAdapter audioManagerAdapter,
+ FeatureFlags flags) {
mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
mLock = lock;
mToneGeneratorFactory = toneGeneratorFactory;
mMediaPlayerFactory = mediaPlayerFactory;
mAudioManagerAdapter = audioManagerAdapter;
+ mFeatureFlags = flags;
}
public void setCallAudioManager(CallAudioManager callAudioManager) {
mCallAudioManager = callAudioManager;
}
- public InCallTonePlayer createPlayer(int tone) {
- return new InCallTonePlayer(tone, mCallAudioManager,
+ public InCallTonePlayer createPlayer(Call call, int tone) {
+ return new InCallTonePlayer(call, tone, mCallAudioManager,
mCallAudioRoutePeripheralAdapter, mLock, mToneGeneratorFactory,
- mMediaPlayerFactory, mAudioManagerAdapter);
+ mMediaPlayerFactory, mAudioManagerAdapter, mFeatureFlags);
}
}
@@ -212,9 +216,11 @@
private Session mSession;
private final Object mSessionLock = new Object();
+ private final Call mCall;
private final ToneGeneratorFactory mToneGenerator;
private final MediaPlayerFactory mMediaPlayerFactory;
private final AudioManagerAdapter mAudioManagerAdapter;
+ private final FeatureFlags mFeatureFlags;
/**
* Latch used for awaiting on playback, which may be interrupted if the tone is stopped from
@@ -228,13 +234,16 @@
* @param toneId ID of the tone to play, see TONE_* constants.
*/
private InCallTonePlayer(
+ Call call,
int toneId,
CallAudioManager callAudioManager,
CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
TelecomSystem.SyncRoot lock,
ToneGeneratorFactory toneGeneratorFactory,
MediaPlayerFactory mediaPlayerFactor,
- AudioManagerAdapter audioManagerAdapter) {
+ AudioManagerAdapter audioManagerAdapter,
+ FeatureFlags flags) {
+ mCall = call;
mState = STATE_OFF;
mToneId = toneId;
mCallAudioManager = callAudioManager;
@@ -243,6 +252,7 @@
mToneGenerator = toneGeneratorFactory;
mMediaPlayerFactory = mediaPlayerFactor;
mAudioManagerAdapter = audioManagerAdapter;
+ mFeatureFlags = flags;
}
/** {@inheritDoc} */
@@ -361,18 +371,8 @@
throw new IllegalStateException("Bad toneId: " + mToneId);
}
- int stream = AudioManager.STREAM_VOICE_CALL;
- if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
- stream = AudioManager.STREAM_BLUETOOTH_SCO;
- }
+ int stream = getStreamType(toneType);
if (toneType != ToneGenerator.TONE_UNKNOWN) {
- if (stream == AudioManager.STREAM_BLUETOOTH_SCO) {
- // Override audio stream for BT le device and hearing aid device
- if (mCallAudioRoutePeripheralAdapter.isLeAudioDeviceOn()
- || mCallAudioRoutePeripheralAdapter.isHearingAidDeviceOn()) {
- stream = AudioManager.STREAM_VOICE_CALL;
- }
- }
playToneGeneratorTone(stream, toneVolume, toneType, toneLengthMillis);
} else if (mediaResourceId != TONE_RESOURCE_ID_UNDEFINED) {
playMediaTone(stream, mediaResourceId);
@@ -384,6 +384,31 @@
}
/**
+ * @param toneType The ToneGenerator tone type
+ * @return The ToneGenerator stream type
+ */
+ private int getStreamType(int toneType) {
+ if (mFeatureFlags.useStreamVoiceCallTones()) {
+ return AudioManager.STREAM_VOICE_CALL;
+ }
+
+ int stream = AudioManager.STREAM_VOICE_CALL;
+ if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
+ stream = AudioManager.STREAM_BLUETOOTH_SCO;
+ }
+ if (toneType != ToneGenerator.TONE_UNKNOWN) {
+ if (stream == AudioManager.STREAM_BLUETOOTH_SCO) {
+ // Override audio stream for BT le device and hearing aid device
+ if (mCallAudioRoutePeripheralAdapter.isLeAudioDeviceOn()
+ || mCallAudioRoutePeripheralAdapter.isHearingAidDeviceOn()) {
+ stream = AudioManager.STREAM_VOICE_CALL;
+ }
+ }
+ }
+ return stream;
+ }
+
+ /**
* Play a tone generated by the {@link ToneGenerator}.
* @param stream The stream on which the tone will be played.
* @param toneVolume The volume of the tone.
@@ -476,7 +501,7 @@
}
if (sTonesPlaying.incrementAndGet() == 1) {
- mCallAudioManager.setIsTonePlaying(true);
+ mCallAudioManager.setIsTonePlaying(mCall, true);
}
synchronized (mSessionLock) {
@@ -524,7 +549,7 @@
Log.i(InCallTonePlayer.this,
"cleanUpTonePlayer(): tonesPlaying=%d, tone completed", newToneCount);
if (mCallAudioManager != null) {
- mCallAudioManager.setIsTonePlaying(false);
+ mCallAudioManager.setIsTonePlaying(mCall, false);
} else {
Log.w(InCallTonePlayer.this,
"cleanUpTonePlayer(): mCallAudioManager is null!");
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
index 0e5a287..b0a7c8e 100644
--- a/src/com/android/server/telecom/MissedCallNotifier.java
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -16,6 +16,7 @@
package com.android.server.telecom;
+import android.annotation.Nullable;
import android.net.Uri;
import android.os.UserHandle;
import android.telecom.PhoneAccountHandle;
@@ -85,7 +86,7 @@
void clearMissedCalls(UserHandle userHandle);
- void showMissedCallNotification(CallInfo call);
+ void showMissedCallNotification(CallInfo call, @Nullable Uri uri);
void reloadAfterBootComplete(CallerInfoLookupHelper callerInfoLookupHelper,
CallInfoFactory callInfoFactory);
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 3b402b1..fce3f1a 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -16,6 +16,7 @@
package com.android.server.telecom;
+import android.Manifest;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
@@ -24,7 +25,6 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Trace;
import android.os.UserHandle;
import android.telecom.GatewayInfo;
import android.telecom.Log;
@@ -37,6 +37,7 @@
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.callredirection.CallRedirectionProcessor;
// TODO: Needed for move to system service: import com.android.internal.R;
@@ -77,6 +78,7 @@
private final TelecomSystem.SyncRoot mLock;
private final DefaultDialerCache mDefaultDialerCache;
private final MmiUtils mMmiUtils;
+ private final FeatureFlags mFeatureFlags;
/*
* Whether or not the outgoing call intent originated from the default phone application. If
@@ -100,7 +102,8 @@
@VisibleForTesting
public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager,
Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
- boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils) {
+ boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils,
+ FeatureFlags featureFlags) {
mContext = context;
mCallsManager = callsManager;
mIntent = intent;
@@ -109,6 +112,7 @@
mLock = mCallsManager.getLock();
mDefaultDialerCache = defaultDialerCache;
mMmiUtils = mmiUtils;
+ mFeatureFlags = featureFlags;
}
/**
@@ -121,14 +125,14 @@
public void onReceive(Context context, Intent intent) {
try {
Log.startSession("NOCBIR.oR");
- Trace.beginSection("onReceiveNewOutgoingCallBroadcast");
synchronized (mLock) {
Log.v(this, "onReceive: %s", intent);
// Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is
// used as the actual number to call. (If null, no call will be placed.)
String resultNumber = getResultData();
- Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
+ Log.i(NewOutgoingCallIntentBroadcaster.this,
+ "Received new-outgoing-call-broadcast for %s with data %s", mCall,
Log.pii(resultNumber));
boolean endEarly = false;
@@ -188,7 +192,6 @@
VideoProfile.STATE_AUDIO_ONLY));
}
} finally {
- Trace.endSection();
Log.endSession();
}
}
@@ -320,6 +323,7 @@
String scheme = mPhoneNumberUtilsAdapter.isUriNumber(number)
? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL;
result.callingAddress = Uri.fromParts(scheme, number, null);
+
return result;
}
@@ -351,14 +355,57 @@
public void processCall(Call call, CallDisposition disposition) {
mCall = call;
+
+ // If the new outgoing call broadast doesn't block, trigger the legacy process call
+ // behavior and exit out here.
+ if (!mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) {
+ legacyProcessCall(disposition);
+ return;
+ }
+ boolean callRedirectionWithService = false;
+ // Only try to do redirection if it was requested and we're not calling immediately.
+ // We can expect callImmediately to be true for emergency calls and voip calls.
+ if (disposition.requestRedirection && !disposition.callImmediately) {
+ CallRedirectionProcessor callRedirectionProcessor = new CallRedirectionProcessor(
+ mContext, mCallsManager, mCall, disposition.callingAddress,
+ mCallsManager.getPhoneAccountRegistrar(),
+ getGateWayInfoFromIntent(mIntent, mIntent.getData()),
+ mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
+ false),
+ mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ VideoProfile.STATE_AUDIO_ONLY));
+ /**
+ * If there is an available {@link android.telecom.CallRedirectionService}, use the
+ * {@link CallRedirectionProcessor} to perform call redirection instead of using
+ * broadcasting.
+ */
+ callRedirectionWithService = callRedirectionProcessor
+ .canMakeCallRedirectionWithServiceAsUser(mCall.getAssociatedUser());
+ if (callRedirectionWithService) {
+ callRedirectionProcessor.performCallRedirection(mCall.getAssociatedUser());
+ }
+ }
+
+ // If no redirection was kicked off, place the call now.
+ if (!callRedirectionWithService) {
+ callImmediately(disposition);
+ }
+
+ // Finally, send the non-blocking broadcast if we're supposed to (ie for any non-voip call).
+ if (disposition.sendBroadcast) {
+ UserHandle targetUser = mCall.getAssociatedUser();
+ broadcastIntent(mIntent, disposition.number, false /* receiverRequired */, targetUser);
+ }
+ }
+
+ /**
+ * The legacy non-flagged version of processing a call. Although there is some code duplication
+ * if makes the new flow cleaner to read.
+ * @param disposition
+ */
+ private void legacyProcessCall(CallDisposition disposition) {
if (disposition.callImmediately) {
- boolean speakerphoneOn = mIntent.getBooleanExtra(
- TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
- int videoState = mIntent.getIntExtra(
- TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
- VideoProfile.STATE_AUDIO_ONLY);
- placeOutgoingCallImmediately(mCall, disposition.callingAddress, null,
- speakerphoneOn, videoState);
+ callImmediately(disposition);
// Don't return but instead continue and send the ACTION_NEW_OUTGOING_CALL broadcast
// so that third parties can still inspect (but not intercept) the outgoing call. When
@@ -390,13 +437,26 @@
if (disposition.sendBroadcast) {
UserHandle targetUser = mCall.getAssociatedUser();
- Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
broadcastIntent(mIntent, disposition.number,
!disposition.callImmediately && !callRedirectionWithService, targetUser);
}
}
/**
+ * Place a call immediately.
+ * @param disposition The disposition; used for retrieving the address of the call.
+ */
+ private void callImmediately(CallDisposition disposition) {
+ boolean speakerphoneOn = mIntent.getBooleanExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+ int videoState = mIntent.getIntExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ VideoProfile.STATE_AUDIO_ONLY);
+ placeOutgoingCallImmediately(mCall, disposition.callingAddress, null,
+ speakerphoneOn, videoState);
+ }
+
+ /**
* Sends a new outgoing call ordered broadcast so that third party apps can cancel the
* placement of the call or redirect it to a different number.
*
@@ -415,28 +475,51 @@
if (number != null) {
broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
}
-
- // Force receivers of this broadcast intent to run at foreground priority because we
- // want to finish processing the broadcast intent as soon as possible.
- broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
- | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
Log.v(this, "Broadcasting intent: %s.", broadcastIntent);
checkAndCopyProviderExtras(originalCallIntent, broadcastIntent);
- final BroadcastOptions options = BroadcastOptions.makeBasic();
- options.setBackgroundActivityStartsAllowed(true);
- mContext.sendOrderedBroadcastAsUser(
- broadcastIntent,
- targetUser,
- android.Manifest.permission.PROCESS_OUTGOING_CALLS,
- AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
- options.toBundle(),
- receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
- null, // scheduler
- Activity.RESULT_OK, // initialCode
- number, // initialData: initial value for the result data (number to be modified)
- null); // initialExtras
+ if (mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) {
+ // Where the new outgoing call broadcast is unblocking, do not give receiver FG priority
+ // and do not allow background activity starts.
+ broadcastIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ Log.i(this, "broadcastIntent: Sending non-blocking for %s to %s", mCall.getId(),
+ targetUser);
+ if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+ mContext.sendBroadcastAsUser(
+ broadcastIntent,
+ targetUser,
+ Manifest.permission.PROCESS_OUTGOING_CALLS);
+ } else {
+ mContext.sendBroadcastAsUser(
+ broadcastIntent,
+ targetUser,
+ android.Manifest.permission.PROCESS_OUTGOING_CALLS,
+ AppOpsManager.OP_PROCESS_OUTGOING_CALLS); // initialExtras
+ }
+ } else {
+ Log.i(this, "broadcastIntent: Sending ordered for %s to %s, waitForResult=%b",
+ mCall.getId(), targetUser, receiverRequired);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setBackgroundActivityStartsAllowed(true);
+ // Force receivers of this broadcast intent to run at foreground priority because we
+ // want to finish processing the broadcast intent as soon as possible.
+ broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+
+ mContext.sendOrderedBroadcastAsUser(
+ broadcastIntent,
+ targetUser,
+ android.Manifest.permission.PROCESS_OUTGOING_CALLS,
+ AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
+ options.toBundle(),
+ receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
+ null, // scheduler
+ Activity.RESULT_OK, // initialCode
+ number, // initialData: initial value for the result data (number to be
+ // modified)
+ null); // initialExtras
+ }
}
/**
@@ -537,6 +620,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/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index 673b99a..3573de8 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -63,6 +63,7 @@
RESTRICTED_CALL_SCREENING_EXTRA_KEYS = new ArrayList<>();
RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(android.telecom.Connection.EXTRA_SIP_INVITE);
RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(ImsCallProfile.EXTRA_IS_BUSINESS_CALL);
+ RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(ImsCallProfile.EXTRA_ASSERTED_DISPLAY_NAME);
}
public static class Converter {
@@ -158,6 +159,10 @@
properties |= android.telecom.Call.Details.PROPERTY_VOIP_AUDIO_MODE;
}
+ if (call.isTransactionalCall()) {
+ properties |= android.telecom.Call.Details.PROPERTY_IS_TRANSACTIONAL;
+ }
+
// If this is a single-SIM device, the "default SIM" will always be the only SIM.
boolean isDefaultSmsAccount = phoneAccountRegistrar != null &&
phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
new file mode 100644
index 0000000..d21ac56
--- /dev/null
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
+import android.bluetooth.BluetoothDevice;
+import android.media.AudioManager;
+import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import java.util.Set;
+
+/**
+ * Used to represent the intermediate state during audio route switching.
+ * Usually, audio route switching start with a communication device setting request to audio
+ * framework and will be completed with corresponding success broadcasts or messages. Instance of
+ * this class is responsible for tracking the pending success signals according to the original
+ * audio route and the destination audio route of this switching.
+ */
+public class PendingAudioRoute {
+ private CallAudioRouteController mCallAudioRouteController;
+ private AudioManager mAudioManager;
+ private BluetoothRouteManager mBluetoothRouteManager;
+ private FeatureFlags mFeatureFlags;
+ /**
+ * The {@link AudioRoute} that this pending audio switching started with
+ */
+ private AudioRoute mOrigRoute;
+ /**
+ * The expected destination {@link AudioRoute} of this pending audio switching, can be changed
+ * by new switching request during the ongoing switching
+ */
+ private AudioRoute mDestRoute;
+ private Set<Pair<Integer, String>> mPendingMessages;
+ private boolean mActive;
+ /**
+ * 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, FeatureFlags featureFlags) {
+ mCallAudioRouteController = controller;
+ mAudioManager = audioManager;
+ mBluetoothRouteManager = bluetoothRouteManager;
+ mFeatureFlags = featureFlags;
+ mPendingMessages = new ArraySet<>();
+ mActive = false;
+ mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+ }
+
+ void setOrigRoute(boolean active, AudioRoute origRoute) {
+ origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
+ mOrigRoute = origRoute;
+ }
+
+ public AudioRoute getOrigRoute() {
+ return mOrigRoute;
+ }
+
+ void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+ boolean isScoAudioConnected) {
+ destRoute.onDestRouteAsPendingRoute(active, this, device,
+ mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
+ mActive = active;
+ mDestRoute = destRoute;
+ }
+
+ public AudioRoute getDestRoute() {
+ return mDestRoute;
+ }
+
+ public void addMessage(int message, String bluetoothDevice) {
+ mPendingMessages.add(new Pair<>(message, bluetoothDevice));
+ }
+
+ 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
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mCallAudioRouteController.fallBack(btAddressToExclude);
+ } else {
+ mCallAudioRouteController.sendMessageWithSessionInfo(
+ SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, btAddressToExclude);
+ }
+ return;
+ }
+
+ // Removes the first occurrence of the specified message from this list, if it is present.
+ mPendingMessages.remove(message);
+ evaluatePendingState();
+ }
+
+ public void evaluatePendingState() {
+ 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 Set<Pair<Integer, String>> getPendingMessages() {
+ return mPendingMessages;
+ }
+
+ 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;
+ }
+
+ public FeatureFlags getFeatureFlags() {
+ return mFeatureFlags;
+ }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index acf07e3..1a1af92 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -58,9 +58,11 @@
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.ModifiedUtf8;
+import com.android.server.telecom.flags.Flags;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -80,10 +82,12 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@@ -180,28 +184,43 @@
private interface PhoneAccountRegistrarWriteLock {}
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) {
- this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy);
+ DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
+ 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) {
+ DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
+ 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;
+ } else {
+ mTelephonyFeatureFlags =
+ new com.android.internal.telephony.flags.FeatureFlagsImpl();
+ }
// register context based receiver to clean up orphan phone accounts
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MANAGED_PROFILE_REMOVED);
@@ -223,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;
}
@@ -389,10 +412,30 @@
account.getGroupId()));
}
- // Potentially update the default voice subid in SubscriptionManager.
+ // Potentially update the default voice subid in SubscriptionManager so that Telephony and
+ // Telecom are in sync.
int newSubId = accountHandle == null ? SubscriptionManager.INVALID_SUBSCRIPTION_ID :
getSubscriptionIdForPhoneAccount(accountHandle);
- if (isSimAccount || accountHandle == null) {
+ if (Flags.onlyUpdateTelephonyOnValidSubIds()) {
+ if (shouldUpdateTelephonyDefaultVoiceSubId(accountHandle, isSimAccount, newSubId)) {
+ updateDefaultVoiceSubId(newSubId, accountHandle);
+ } else {
+ Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+ }
+ } else {
+ if (isSimAccount || accountHandle == null) {
+ updateDefaultVoiceSubId(newSubId, accountHandle);
+ } else {
+ Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+ }
+ }
+
+ write();
+ fireDefaultOutgoingChanged();
+ }
+
+ private void updateDefaultVoiceSubId(int newSubId, PhoneAccountHandle accountHandle){
+ try {
int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId();
if (newSubId != currentVoiceSubId) {
Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; "
@@ -401,17 +444,43 @@
} else {
Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub");
}
- } else {
- Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+ } catch (UnsupportedOperationException uoe) {
+ Log.w(this, "setUserSelectedOutgoingPhoneAccount: no telephony");
}
+ }
- write();
- fireDefaultOutgoingChanged();
+ // This helper is important for CTS testing. [PhoneAccount]s created by Telecom in CTS are
+ // assigned a subId value of INVALID_SUBSCRIPTION_ID (-1) by Telephony. However, when
+ // Telephony has a default outgoing calling voice account of -1, that translates to no default
+ // account (user should be prompted to select an acct when making MOs). In order to avoid
+ // Telephony clearing out the newly changed default [PhoneAccount] in Telecom, Telephony should
+ // not be updated. This situation will never occur in production since [PhoneAccount]s in
+ // production are assigned non-negative subId values.
+ private boolean shouldUpdateTelephonyDefaultVoiceSubId(PhoneAccountHandle phoneAccountHandle,
+ boolean isSimAccount, int newSubId) {
+ // user requests no call preference
+ if (phoneAccountHandle == null) {
+ return true;
+ }
+ // do not update Telephony if the newSubId is invalid
+ if (newSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ Log.w(this, "shouldUpdateTelephonyDefaultVoiceSubId: "
+ + "invalid subId scenario, not updating Telephony. "
+ + "phoneAccountHandle=[%s], isSimAccount=[%b], newSubId=[%s]",
+ phoneAccountHandle, isSimAccount, newSubId);
+ return false;
+ }
+ return isSimAccount;
}
boolean isUserSelectedSmsPhoneAccount(PhoneAccountHandle accountHandle) {
- return getSubscriptionIdForPhoneAccount(accountHandle) ==
- SubscriptionManager.getDefaultSmsSubscriptionId();
+ try {
+ return getSubscriptionIdForPhoneAccount(accountHandle) ==
+ SubscriptionManager.getDefaultSmsSubscriptionId();
+ } catch (UnsupportedOperationException uoe) {
+ Log.w(this, "isUserSelectedSmsPhoneAccount: no telephony");
+ return false;
+ }
}
public ComponentName getSystemSimCallManagerComponent() {
@@ -420,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);
@@ -660,6 +735,24 @@
}
}
+ private boolean isMatchedUser(PhoneAccount account, UserHandle userHandle) {
+ if (account == null) {
+ return false;
+ }
+
+ if (userHandle == null) {
+ Log.w(this, "userHandle is null in isVisibleForUser");
+ return false;
+ }
+
+ UserHandle phoneAccountUserHandle = account.getAccountHandle().getUserHandle();
+ if (phoneAccountUserHandle == null) {
+ return false;
+ }
+
+ return phoneAccountUserHandle.equals(userHandle);
+ }
+
private boolean isVisibleForUser(PhoneAccount account, UserHandle userHandle,
boolean acrossProfiles) {
if (account == null) {
@@ -689,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);
}
@@ -726,11 +822,11 @@
*/
public List<PhoneAccountHandle> getAllPhoneAccountHandles(UserHandle userHandle,
boolean crossUserAccess) {
- return getPhoneAccountHandles(0, null, null, false, userHandle, crossUserAccess);
+ return getPhoneAccountHandles(0, null, null, false, userHandle, crossUserAccess, true);
}
public List<PhoneAccount> getAllPhoneAccounts(UserHandle userHandle, boolean crossUserAccess) {
- return getPhoneAccounts(0, null, null, false, mCurrentUserHandle, crossUserAccess);
+ return getPhoneAccounts(0, null, null, false, mCurrentUserHandle, crossUserAccess, true);
}
/**
@@ -821,7 +917,7 @@
public List<PhoneAccountHandle> getAllPhoneAccountHandlesForPackage(UserHandle userHandle,
String packageName) {
return getPhoneAccountHandles(0, null, packageName, true /* includeDisabled */, userHandle,
- true /* crossUserAccess */);
+ true /* crossUserAccess */, true);
}
/**
@@ -885,11 +981,36 @@
}
enforceCharacterLimit(account);
enforceIconSizeLimit(account);
+ if (mTelecomFeatureFlags.unregisterUnresolvableAccounts()) {
+ enforcePhoneAccountTargetService(account);
+ }
enforceMaxPhoneAccountLimit(account);
+ if (mTelephonyFeatureFlags.simultaneousCallingIndications()) {
+ enforceSimultaneousCallingRestrictionLimit(account);
+ }
addOrReplacePhoneAccount(account);
}
/**
+ * This method ensures that {@link PhoneAccount}s that have the {@link
+ * PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} capability are not
+ * backed by a {@link ConnectionService}
+ *
+ * @param account enforce the check on
+ */
+ private void enforcePhoneAccountTargetService(PhoneAccount account) {
+ if (phoneAccountRequiresBindPermission(account.getAccountHandle()) &&
+ hasTransactionalCallCapabilities(account)) {
+ throw new IllegalArgumentException(
+ "Error, the PhoneAccount you are registering has"
+ + " CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS and the"
+ + " PhoneAccountHandle's ComponentName#ClassName points to a"
+ + " ConnectionService class. Either remove the capability or use a"
+ + " different ClassName in the PhoneAccountHandle.");
+ }
+ }
+
+ /**
* Enforce an upper bound on the number of PhoneAccount's a package can register.
* Most apps should only require 1-2. * Include disabled accounts.
*
@@ -897,13 +1018,17 @@
* @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached
*/
private void enforceMaxPhoneAccountLimit(@NonNull PhoneAccount account) {
- final PhoneAccountHandle accountHandle = account.getAccountHandle();
- final UserHandle user = accountHandle.getUserHandle();
- final ComponentName componentName = accountHandle.getComponentName();
-
- if (getPhoneAccountHandles(0, null, componentName.getPackageName(),
- true /* includeDisabled */, user, false /* crossUserAccess */).size()
- >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+ int numOfAcctsRegisteredForPackage = mTelecomFeatureFlags.unregisterUnresolvableAccounts()
+ ? cleanupAndGetVerifiedAccounts(account).size()
+ : getPhoneAccountHandles(
+ 0/* capabilities */,
+ null /* uriScheme */,
+ account.getAccountHandle().getComponentName().getPackageName(),
+ true /* includeDisabled */,
+ account.getAccountHandle().getUserHandle(),
+ false /* crossUserAccess */).size();
+ // enforce the max phone account limit for the application registering accounts
+ if (numOfAcctsRegisteredForPackage >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
"enforceMaxPhoneAccountLimit");
throw new IllegalArgumentException(
@@ -913,6 +1038,64 @@
}
}
+ @VisibleForTesting
+ public List<PhoneAccount> getRegisteredAccountsForPackageName(String packageName,
+ UserHandle userHandle) {
+ if (packageName == null) {
+ return new ArrayList<>();
+ }
+ List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
+ for (PhoneAccount m : mState.accounts) {
+ PhoneAccountHandle handle = m.getAccountHandle();
+ if (!packageName.equals(handle.getComponentName().getPackageName())) {
+ // Not the right package name; skip this one.
+ continue;
+ }
+ // Do not count accounts registered under different users on the device. Otherwise, an
+ // application can only have MAX_PHONE_ACCOUNT_REGISTRATIONS across all users. If the
+ // DUT has multiple users, they should each get to register 10 accounts. Also, 3rd
+ // party applications cannot create new UserHandles without highly privileged
+ // permissions.
+ if (!isVisibleForUser(m, userHandle, false)) {
+ // Account is not visible for the current user; skip this one.
+ continue;
+ }
+ accounts.add(m);
+ }
+ return accounts;
+ }
+
+ /**
+ * Unregister {@link ConnectionService} accounts that no longer have a resolvable Service. This
+ * means the Service has been disabled or died. Skip the verification for transactional
+ * accounts.
+ *
+ * @param newAccount being registered
+ * @return all the verified accounts. These accounts are now guaranteed to be backed by a
+ * {@link ConnectionService} or do not need one (transactional accounts).
+ */
+ @VisibleForTesting
+ public List<PhoneAccount> cleanupAndGetVerifiedAccounts(PhoneAccount newAccount) {
+ ArrayList<PhoneAccount> verifiedAccounts = new ArrayList<>();
+ List<PhoneAccount> unverifiedAccounts = getRegisteredAccountsForPackageName(
+ newAccount.getAccountHandle().getComponentName().getPackageName(),
+ newAccount.getAccountHandle().getUserHandle());
+ for (PhoneAccount account : unverifiedAccounts) {
+ PhoneAccountHandle handle = account.getAccountHandle();
+ if (/* skip for transactional accounts since they don't require a ConnectionService */
+ !hasTransactionalCallCapabilities(account) &&
+ /* check if the {@link ConnectionService} has been disabled or can longer be
+ found */ resolveComponent(handle).isEmpty()) {
+ Log.i(this, " cAGVA: Cannot resolve the ConnectionService for"
+ + " handle=[%s]; unregistering account", handle);
+ unregisterPhoneAccount(handle);
+ } else {
+ verifiedAccounts.add(account);
+ }
+ }
+ return verifiedAccounts;
+ }
+
/**
* determine if there will be an issue writing the icon to memory
*
@@ -1014,6 +1197,43 @@
}
/**
+ * Enforce size limits on the simultaneous calling restriction of a PhoneAccount.
+ * If a PhoneAccount has a simultaneous calling restriction on it, enforce the following: the
+ * number of PhoneAccountHandles in the Set can not exceed the per app restriction on
+ * PhoneAccounts registered and each PhoneAccountHandle's fields must not exceed the per field
+ * character limit.
+ * @param account The PhoneAccount to enforce simultaneous calling restrictions on.
+ * @throws IllegalArgumentException if the PhoneAccount exceeds size limits.
+ */
+ public void enforceSimultaneousCallingRestrictionLimit(@NonNull PhoneAccount account) {
+ if (!account.hasSimultaneousCallingRestriction()) return;
+ Set<PhoneAccountHandle> restrictions = account.getSimultaneousCallingRestriction();
+ if (restrictions.size() > MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+ throw new IllegalArgumentException("Can not register a PhoneAccount with a number"
+ + "of simultaneous calling restrictions that is greater than "
+ + MAX_PHONE_ACCOUNT_REGISTRATIONS);
+ }
+ for (PhoneAccountHandle handle : restrictions) {
+ ComponentName component = handle.getComponentName();
+ if (component.getPackageName().length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+ throw new IllegalArgumentException("A PhoneAccountHandle added as part of "
+ + "a simultaneous calling restriction has a package name that has exceeded "
+ + "the character limit of " + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT);
+ }
+ if (component.getClassName().length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+ throw new IllegalArgumentException("A PhoneAccountHandle added as part of "
+ + "a simultaneous calling restriction has a class name that has exceeded "
+ + "the character limit of " + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT);
+ }
+ if (handle.getId().length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+ throw new IllegalArgumentException("A PhoneAccountHandle added as part of "
+ + "a simultaneous calling restriction has an ID that has exceeded "
+ + "the character limit of " + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT);
+ }
+ }
+ }
+
+ /**
* Enforce a character limit on all PA and PAH string or char-sequence fields.
*
* @param account to enforce check on
@@ -1064,12 +1284,15 @@
boolean isNewAccount;
// add self-managed capability for transactional accounts that are missing it
- if (hasTransactionalCallCapabilities(account) &&
- !account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+ if (hasTransactionalCallCapabilities(account)
+ && !account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
account = account.toBuilder()
.setCapabilities(account.getCapabilities()
| PhoneAccount.CAPABILITY_SELF_MANAGED)
.build();
+ // Note: below we will automatically remove CAPABILITY_CONNECTION_MANAGER,
+ // CAPABILITY_CALL_PROVIDER, and CAPABILITY_SIM_SUBSCRIPTION if this magically becomes
+ // a self-managed phone account here.
}
PhoneAccount oldAccount = getPhoneAccountUnchecked(account.getAccountHandle());
@@ -1090,6 +1313,12 @@
if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
// Turn off bits we don't want to be able to set (TelecomServiceImpl protects against
// this but we'll also prevent it from happening here, just to be safe).
+ if ((account.getCapabilities() & (PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+ | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) > 0) {
+ Log.w(this, "addOrReplacePhoneAccount: attempt to register a "
+ + "VoIP phone account with call provider/cm/sim sub capabilities.");
+ }
int newCapabilities = account.getCapabilities() &
~(PhoneAccount.CAPABILITY_CALL_PROVIDER |
PhoneAccount.CAPABILITY_CONNECTION_MANAGER |
@@ -1097,7 +1326,10 @@
// Ensure name is correct.
CharSequence newLabel = mAppLabelProxy.getAppLabel(
- account.getAccountHandle().getComponentName().getPackageName());
+ account.getAccountHandle().getComponentName().getPackageName(),
+ UserUtil.getAssociatedUserForCall(
+ mTelecomFeatureFlags.associatedUserRefactorForWorkProfile(),
+ this, UserHandle.CURRENT, account.getAccountHandle()));
account = account.toBuilder()
.setLabel(newLabel)
@@ -1348,16 +1580,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");
}
}
@@ -1451,7 +1689,19 @@
UserHandle userHandle,
boolean crossUserAccess) {
return getPhoneAccountHandles(capabilities, 0 /*excludedCapabilities*/, uriScheme,
- packageName, includeDisabledAccounts, userHandle, crossUserAccess);
+ packageName, includeDisabledAccounts, userHandle, crossUserAccess, false);
+ }
+
+ private List<PhoneAccountHandle> getPhoneAccountHandles(
+ int capabilities,
+ String uriScheme,
+ String packageName,
+ boolean includeDisabledAccounts,
+ UserHandle userHandle,
+ boolean crossUserAccess,
+ boolean includeAll) {
+ return getPhoneAccountHandles(capabilities, 0 /*excludedCapabilities*/, uriScheme,
+ packageName, includeDisabledAccounts, userHandle, crossUserAccess, includeAll);
}
/**
@@ -1466,11 +1716,24 @@
boolean includeDisabledAccounts,
UserHandle userHandle,
boolean crossUserAccess) {
+ return getPhoneAccountHandles(capabilities, excludedCapabilities, uriScheme, packageName,
+ includeDisabledAccounts, userHandle, crossUserAccess, false);
+ }
+
+ private List<PhoneAccountHandle> getPhoneAccountHandles(
+ int capabilities,
+ int excludedCapabilities,
+ String uriScheme,
+ String packageName,
+ boolean includeDisabledAccounts,
+ UserHandle userHandle,
+ boolean crossUserAccess,
+ boolean includeAll) {
List<PhoneAccountHandle> handles = new ArrayList<>();
for (PhoneAccount account : getPhoneAccounts(
capabilities, excludedCapabilities, uriScheme, packageName,
- includeDisabledAccounts, userHandle, crossUserAccess)) {
+ includeDisabledAccounts, userHandle, crossUserAccess, includeAll)) {
handles.add(account.getAccountHandle());
}
return handles;
@@ -1484,7 +1747,19 @@
UserHandle userHandle,
boolean crossUserAccess) {
return getPhoneAccounts(capabilities, 0 /*excludedCapabilities*/, uriScheme, packageName,
- includeDisabledAccounts, userHandle, crossUserAccess);
+ includeDisabledAccounts, userHandle, crossUserAccess, false);
+ }
+
+ private List<PhoneAccount> getPhoneAccounts(
+ int capabilities,
+ String uriScheme,
+ String packageName,
+ boolean includeDisabledAccounts,
+ UserHandle userHandle,
+ boolean crossUserAccess,
+ boolean includeAll) {
+ return getPhoneAccounts(capabilities, 0 /*excludedCapabilities*/, uriScheme, packageName,
+ includeDisabledAccounts, userHandle, crossUserAccess, includeAll);
}
/**
@@ -1506,7 +1781,22 @@
boolean includeDisabledAccounts,
UserHandle userHandle,
boolean crossUserAccess) {
+ return getPhoneAccounts(capabilities, excludedCapabilities, uriScheme, packageName,
+ includeDisabledAccounts, userHandle, crossUserAccess, false);
+ }
+
+ @VisibleForTesting
+ public List<PhoneAccount> getPhoneAccounts(
+ int capabilities,
+ int excludedCapabilities,
+ String uriScheme,
+ String packageName,
+ boolean includeDisabledAccounts,
+ UserHandle userHandle,
+ boolean crossUserAccess,
+ boolean includeAll) {
List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
+ List<PhoneAccount> matchedAccounts = new ArrayList<>(mState.accounts.size());
for (PhoneAccount m : mState.accounts) {
if (!(m.isEnabled() || includeDisabledAccounts)) {
// Do not include disabled accounts.
@@ -1540,12 +1830,22 @@
// Not the right package name; skip this one.
continue;
}
+ if (isMatchedUser(m, userHandle)) {
+ matchedAccounts.add(m);
+ }
if (!crossUserAccess && !isVisibleForUser(m, userHandle, false)) {
// Account is not visible for the current user; skip this one.
continue;
}
accounts.add(m);
}
+
+ // Return the account if it exactly matches. Otherwise, return any account that's visible
+ if (mTelephonyFeatureFlags.workProfileApiSplit() && !crossUserAccess && !includeAll
+ && !matchedAccounts.isEmpty()) {
+ return matchedAccounts;
+ }
+
return accounts;
}
@@ -1662,6 +1962,12 @@
} else {
pw.println(defaultOutgoing);
}
+ // 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();
@@ -1750,7 +2056,7 @@
sortPhoneAccounts();
ByteArrayOutputStream os = new ByteArrayOutputStream();
XmlSerializer serializer = Xml.resolveSerializer(os);
- writeToXml(mState, serializer, mContext);
+ writeToXml(mState, serializer, mContext, mTelephonyFeatureFlags);
serializer.flush();
new AsyncXmlWriter().execute(os);
} catch (IOException e) {
@@ -1771,7 +2077,7 @@
try {
XmlPullParser parser = Xml.resolvePullParser(is);
parser.nextTag();
- mState = readFromXml(parser, mContext);
+ mState = readFromXml(parser, mContext, mTelephonyFeatureFlags, mTelecomFeatureFlags);
migratePhoneAccountHandle(mState);
versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
@@ -1806,14 +2112,17 @@
}
}
- private static void writeToXml(State state, XmlSerializer serializer, Context context)
- throws IOException {
- sStateXml.writeToXml(state, serializer, context);
+ private static void writeToXml(State state, XmlSerializer serializer, Context context,
+ FeatureFlags telephonyFeatureFlags) throws IOException {
+ sStateXml.writeToXml(state, serializer, context, telephonyFeatureFlags);
}
- private static State readFromXml(XmlPullParser parser, Context context)
+ private static State readFromXml(XmlPullParser parser, Context context,
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws IOException, XmlPullParserException {
- State s = sStateXml.readFromXml(parser, 0, context);
+ State s = sStateXml.readFromXml(parser, 0, context,
+ telephonyFeatureFlags, telecomFeatureFlags);
return s != null ? s : new State();
}
@@ -1879,8 +2188,8 @@
/**
* Write the supplied object to XML
*/
- public abstract void writeToXml(T o, XmlSerializer serializer, Context context)
- throws IOException;
+ public abstract void writeToXml(T o, XmlSerializer serializer, Context context,
+ FeatureFlags telephonyFeatureFlags) throws IOException;
/**
* Read from the supplied XML into a new object, returning null in case of an
@@ -1889,7 +2198,9 @@
* object's writeToXml(). This object tries to fail early without modifying
* 'parser' if it does not recognize the data it sees.
*/
- public abstract T readFromXml(XmlPullParser parser, int version, Context context)
+ public abstract T readFromXml(XmlPullParser parser, int version, Context context,
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags featureFlags)
throws IOException, XmlPullParserException;
protected void writeTextIfNonNull(String tagName, Object value, XmlSerializer serializer)
@@ -1902,6 +2213,29 @@
}
/**
+ * Serializes a List of PhoneAccountHandles.
+ * @param tagName The tag for the List
+ * @param handles The List of PhoneAccountHandles to serialize
+ * @param serializer The serializer
+ * @throws IOException if serialization fails.
+ */
+ protected void writePhoneAccountHandleSet(String tagName, Set<PhoneAccountHandle> handles,
+ XmlSerializer serializer, Context context, FeatureFlags telephonyFeatureFlags)
+ throws IOException {
+ serializer.startTag(null, tagName);
+ if (handles != null) {
+ serializer.attribute(null, ATTRIBUTE_LENGTH, Objects.toString(handles.size()));
+ for (PhoneAccountHandle handle : handles) {
+ sPhoneAccountHandleXml.writeToXml(handle, serializer, context,
+ telephonyFeatureFlags);
+ }
+ } else {
+ serializer.attribute(null, ATTRIBUTE_LENGTH, "0");
+ }
+ serializer.endTag(null, tagName);
+ }
+
+ /**
* Serializes a string array.
*
* @param tagName The tag name for the string array.
@@ -1995,6 +2329,22 @@
serializer.endTag(null, tagName);
}
+ protected Set<PhoneAccountHandle> readPhoneAccountHandleSet(XmlPullParser parser,
+ 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);
+ if (length == 0) return handles;
+
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ handles.add(sPhoneAccountHandleXml.readFromXml(parser, version, context,
+ telephonyFeatureFlags, telecomFeatureFlags));
+ }
+ return handles;
+ }
+
/**
* Reads a string array from the XML parser.
*
@@ -2102,8 +2452,8 @@
private static final String VERSION = "version";
@Override
- public void writeToXml(State o, XmlSerializer serializer, Context context)
- throws IOException {
+ public void writeToXml(State o, XmlSerializer serializer, Context context,
+ FeatureFlags telephonyFeatureFlags) throws IOException {
if (o != null) {
serializer.startTag(null, CLASS_STATE);
serializer.attribute(null, VERSION, Objects.toString(EXPECTED_STATE_VERSION));
@@ -2111,14 +2461,15 @@
serializer.startTag(null, DEFAULT_OUTGOING);
for (DefaultPhoneAccountHandle defaultPhoneAccountHandle : o
.defaultOutgoingAccountHandles.values()) {
- sDefaultPhoneAcountHandleXml
- .writeToXml(defaultPhoneAccountHandle, serializer, context);
+ sDefaultPhoneAccountHandleXml
+ .writeToXml(defaultPhoneAccountHandle, serializer, context,
+ telephonyFeatureFlags);
}
serializer.endTag(null, DEFAULT_OUTGOING);
serializer.startTag(null, ACCOUNTS);
for (PhoneAccount m : o.accounts) {
- sPhoneAccountXml.writeToXml(m, serializer, context);
+ sPhoneAccountXml.writeToXml(m, serializer, context, telephonyFeatureFlags);
}
serializer.endTag(null, ACCOUNTS);
@@ -2127,7 +2478,9 @@
}
@Override
- public State readFromXml(XmlPullParser parser, int version, Context context)
+ public State readFromXml(XmlPullParser parser, int version, Context context,
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws IOException, XmlPullParserException {
if (parser.getName().equals(CLASS_STATE)) {
State s = new State();
@@ -2144,23 +2497,32 @@
// assume there are no groups.
parser.nextTag();
PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
- .readFromXml(parser, s.versionNumber, context);
- UserManager userManager = UserManager.get(context);
- UserInfo primaryUser = userManager.getPrimaryUser();
+ .readFromXml(parser, s.versionNumber, context,
+ 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();
while (XmlUtils.nextElementWithin(parser, defaultAccountHandlesDepth)) {
DefaultPhoneAccountHandle accountHandle
- = sDefaultPhoneAcountHandleXml
- .readFromXml(parser, s.versionNumber, context);
+ = sDefaultPhoneAccountHandleXml
+ .readFromXml(parser, s.versionNumber, context,
+ telephonyFeatureFlags, telecomFeatureFlags);
if (accountHandle != null && s.accounts != null) {
s.defaultOutgoingAccountHandles
.put(accountHandle.userHandle, accountHandle);
@@ -2171,7 +2533,8 @@
int accountsDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
- s.versionNumber, context);
+ s.versionNumber, context, telephonyFeatureFlags,
+ telecomFeatureFlags);
if (account != null && s.accounts != null) {
s.accounts.add(account);
@@ -2186,7 +2549,7 @@
};
@VisibleForTesting
- public static final XmlSerialization<DefaultPhoneAccountHandle> sDefaultPhoneAcountHandleXml =
+ public static final XmlSerialization<DefaultPhoneAccountHandle> sDefaultPhoneAccountHandleXml =
new XmlSerialization<DefaultPhoneAccountHandle>() {
private static final String CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE
= "default_outgoing_phone_account_handle";
@@ -2196,9 +2559,9 @@
@Override
public void writeToXml(DefaultPhoneAccountHandle o, XmlSerializer serializer,
- Context context) throws IOException {
+ 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);
@@ -2206,7 +2569,7 @@
writeNonNullString(GROUP_ID, o.groupId, serializer);
serializer.startTag(null, ACCOUNT_HANDLE);
sPhoneAccountHandleXml.writeToXml(o.phoneAccountHandle, serializer,
- context);
+ context, telephonyFeatureFlags);
serializer.endTag(null, ACCOUNT_HANDLE);
serializer.endTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
}
@@ -2215,7 +2578,8 @@
@Override
public DefaultPhoneAccountHandle readFromXml(XmlPullParser parser, int version,
- Context context)
+ 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();
@@ -2226,7 +2590,7 @@
if (parser.getName().equals(ACCOUNT_HANDLE)) {
parser.nextTag();
accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
- context);
+ context, telephonyFeatureFlags, telecomFeatureFlags);
} else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
parser.next();
userSerialNumberString = parser.getText();
@@ -2240,7 +2604,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,
@@ -2277,16 +2641,19 @@
private static final String ICON = "icon";
private static final String EXTRAS = "extras";
private static final String ENABLED = "enabled";
+ private static final String SIMULTANEOUS_CALLING_RESTRICTION
+ = "simultaneous_calling_restriction";
@Override
- public void writeToXml(PhoneAccount o, XmlSerializer serializer, Context context)
- throws IOException {
+ public void writeToXml(PhoneAccount o, XmlSerializer serializer, Context context,
+ FeatureFlags telephonyFeatureFlags) throws IOException {
if (o != null) {
serializer.startTag(null, CLASS_PHONE_ACCOUNT);
if (o.getAccountHandle() != null) {
serializer.startTag(null, ACCOUNT_HANDLE);
- sPhoneAccountHandleXml.writeToXml(o.getAccountHandle(), serializer, context);
+ sPhoneAccountHandleXml.writeToXml(o.getAccountHandle(), serializer, context,
+ telephonyFeatureFlags);
serializer.endTag(null, ACCOUNT_HANDLE);
}
@@ -2303,13 +2670,20 @@
writeTextIfNonNull(ENABLED, o.isEnabled() ? "true" : "false" , serializer);
writeTextIfNonNull(SUPPORTED_AUDIO_ROUTES, Integer.toString(
o.getSupportedAudioRoutes()), serializer);
+ if (o.hasSimultaneousCallingRestriction()
+ && telephonyFeatureFlags.simultaneousCallingIndications()) {
+ writePhoneAccountHandleSet(SIMULTANEOUS_CALLING_RESTRICTION,
+ o.getSimultaneousCallingRestriction(), serializer, context,
+ telephonyFeatureFlags);
+ }
serializer.endTag(null, CLASS_PHONE_ACCOUNT);
}
}
- public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context)
- throws IOException, XmlPullParserException {
+ public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context,
+ 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;
@@ -2328,12 +2702,13 @@
Icon icon = null;
boolean enabled = false;
Bundle extras = null;
+ Set<PhoneAccountHandle> simultaneousCallingRestriction = null;
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (parser.getName().equals(ACCOUNT_HANDLE)) {
parser.nextTag();
accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
- context);
+ context, telephonyFeatureFlags, telecomFeatureFlags);
} else if (parser.getName().equals(ADDRESS)) {
parser.next();
address = Uri.parse(parser.getText());
@@ -2378,6 +2753,12 @@
} else if (parser.getName().equals(SUPPORTED_AUDIO_ROUTES)) {
parser.next();
supportedAudioRoutes = Integer.parseInt(parser.getText());
+ } else if (parser.getName().equals(SIMULTANEOUS_CALLING_RESTRICTION)) {
+ // We can not flag this because we always need to handle the case where
+ // 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, telecomFeatureFlags);
}
}
@@ -2459,6 +2840,9 @@
} else if (!TextUtils.isEmpty(iconPackageName)) {
builder.setIcon(Icon.createWithResource(iconPackageName, iconResId));
// TODO: Need to set tint.
+ } else if (simultaneousCallingRestriction != null
+ && telephonyFeatureFlags.simultaneousCallingIndications()) {
+ builder.setSimultaneousCallingRestriction(simultaneousCallingRestriction);
}
return builder.build();
@@ -2490,8 +2874,8 @@
private static final String USER_SERIAL_NUMBER = "user_serial_number";
@Override
- public void writeToXml(PhoneAccountHandle o, XmlSerializer serializer, Context context)
- throws IOException {
+ public void writeToXml(PhoneAccountHandle o, XmlSerializer serializer, Context context,
+ FeatureFlags telephonyFeatureFlags) throws IOException {
if (o != null) {
serializer.startTag(null, CLASS_PHONE_ACCOUNT_HANDLE);
@@ -2503,7 +2887,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);
}
@@ -2513,7 +2897,9 @@
}
@Override
- public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context)
+ public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context,
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws IOException, XmlPullParserException {
if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
String componentNameString = null;
@@ -2521,7 +2907,7 @@
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)) {
@@ -2554,8 +2940,4 @@
return null;
}
};
-
- private String nullToEmpty(String str) {
- return str == null ? "" : str;
- }
}
diff --git a/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java b/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java
index 438ee68..ab55703 100644
--- a/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java
+++ b/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java
@@ -27,6 +27,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.telecom.PhoneAccountHandle;
@@ -46,6 +47,7 @@
public class PhoneAccountSuggestionHelper {
private static final String TAG = PhoneAccountSuggestionHelper.class.getSimpleName();
private static ComponentName sOverrideComponent;
+ private static UserHandle sOverrideUserHandle;
/**
* @return A future (possible already complete) that contains a list of suggestions.
@@ -53,6 +55,15 @@
public static CompletableFuture<List<PhoneAccountSuggestion>>
bindAndGetSuggestions(Context context, Uri handle,
List<PhoneAccountHandle> availablePhoneAccounts) {
+ Context userContext;
+ if (sOverrideUserHandle != null) {
+ userContext = context.createContextAsUser(sOverrideUserHandle, 0);
+ Log.i(TAG, "bindAndGetSuggestions created context as user; userContext=%s",
+ userContext);
+ } else {
+ userContext = context;
+ }
+
// Use the default list if there's no handle
if (handle == null) {
return CompletableFuture.completedFuture(getDefaultSuggestions(availablePhoneAccounts));
@@ -60,7 +71,7 @@
String number = PhoneNumberUtils.extractNetworkPortion(handle.getSchemeSpecificPart());
// Use the default list if there's no service on the device.
- ServiceInfo suggestionServiceInfo = getSuggestionServiceInfo(context);
+ ServiceInfo suggestionServiceInfo = getSuggestionServiceInfo(userContext);
if (suggestionServiceInfo == null) {
return CompletableFuture.completedFuture(getDefaultSuggestions(availablePhoneAccounts));
}
@@ -124,7 +135,7 @@
}
};
- if (!context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)) {
+ if (!userContext.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)) {
Log.i(TAG, "Cancelling suggestion process due to bind failure.");
future.complete(getDefaultSuggestions(availablePhoneAccounts));
}
@@ -143,7 +154,7 @@
Log.endSession();
}
},
- Timeouts.getPhoneAccountSuggestionServiceTimeout(context.getContentResolver()));
+ Timeouts.getPhoneAccountSuggestionServiceTimeout(userContext.getContentResolver()));
return future;
}
@@ -162,10 +173,25 @@
}
private static ServiceInfo getSuggestionServiceInfo(Context context) {
- PackageManager packageManager = context.getPackageManager();
+ Context userContext;
+ if (sOverrideUserHandle != null) {
+ userContext = context.createContextAsUser(sOverrideUserHandle, 0);
+ Log.i(TAG, "getSuggestionServiceInfo: Created context as user; userContext= %s",
+ userContext);
+ } else {
+ userContext = context;
+ }
+
+ PackageManager packageManager = userContext.getPackageManager();
+
Intent queryIntent = new Intent();
queryIntent.setAction(PhoneAccountSuggestionService.SERVICE_INTERFACE);
+ if (packageManager == null) {
+ Log.i(TAG, "getSuggestionServiceInfo: PackageManager is null. Using defaults.");
+ return null;
+ }
+
List<ResolveInfo> services;
if (sOverrideComponent == null) {
services = packageManager.queryIntentServices(queryIntent,
@@ -199,6 +225,15 @@
}
}
+ static void setOverrideUserHandle(UserHandle userHandle) {
+ try {
+ sOverrideUserHandle = userHandle;
+ } catch (Exception e) {
+ sOverrideUserHandle = null;
+ throw e;
+ }
+ }
+
private static List<PhoneAccountSuggestion> getDefaultSuggestions(
List<PhoneAccountHandle> phoneAccountHandles) {
return phoneAccountHandles.stream().map(phoneAccountHandle ->
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/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 1d42db4..2dcd093 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -27,7 +27,6 @@
import android.content.res.Resources;
import android.telecom.Connection;
import android.telecom.Log;
-import android.telecom.Response;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import android.telephony.SubscriptionManager;
@@ -92,7 +91,7 @@
* the main thread.
* @param context The context.
*/
- public void loadCannedTextMessages(final Response<Void, List<String>> response,
+ public void loadCannedTextMessages(final CallsManager.Response<Void, List<String>> response,
final Context context) {
new Thread() {
@Override
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
index a8af3ac..5ace9ba 100644
--- a/src/com/android/server/telecom/RingbackPlayer.java
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -19,6 +19,7 @@
import static com.android.server.telecom.LogUtils.Events.START_RINBACK;
import static com.android.server.telecom.LogUtils.Events.STOP_RINGBACK;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import android.telecom.Log;
@@ -42,8 +43,12 @@
*/
private InCallTonePlayer mTonePlayer;
- RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
+ private final Object mLock;
+
+ @VisibleForTesting
+ public RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
mPlayerFactory = playerFactory;
+ mLock = new Object();
}
/**
@@ -52,25 +57,27 @@
* @param call The call for which to ringback.
*/
public void startRingbackForCall(Call call) {
- Preconditions.checkState(call.getState() == CallState.DIALING);
+ synchronized (mLock) {
+ Preconditions.checkState(call.getState() == CallState.DIALING);
- if (mCall == call) {
- Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
- return;
- }
+ if (mCall == call) {
+ Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
+ return;
+ }
- if (mCall != null) {
- // We only get here for the foreground call so, there's no reason why there should
- // exist a current dialing call.
- Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
- }
+ if (mCall != null) {
+ // We only get here for the foreground call so, there's no reason why there should
+ // exist a current dialing call.
+ Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
+ }
- mCall = call;
- if (mTonePlayer == null) {
- Log.i(this, "Playing the ringback tone for %s.", call);
- Log.addEvent(call, START_RINBACK);
- mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
- mTonePlayer.startTone();
+ mCall = call;
+ if (mTonePlayer == null) {
+ Log.i(this, "Playing the ringback tone for %s.", call);
+ Log.addEvent(call, START_RINBACK);
+ mTonePlayer = mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_RING_BACK);
+ mTonePlayer.startTone();
+ }
}
}
@@ -80,19 +87,27 @@
* @param call The call for which to stop ringback.
*/
public void stopRingbackForCall(Call call) {
- if (mCall == call) {
- // The foreground call is no longer dialing or is no longer the foreground call. In
- // either case, stop the ringback tone.
- mCall = null;
+ synchronized (mLock) {
+ if (mCall == call) {
+ // The foreground call is no longer dialing or is no longer the foreground call. In
+ // either case, stop the ringback tone.
+ mCall = null;
- if (mTonePlayer == null) {
- Log.w(this, "No player found to stop.");
- } else {
- Log.i(this, "Stopping the ringback tone for %s.", call);
- Log.addEvent(call, STOP_RINGBACK);
- mTonePlayer.stopTone();
- mTonePlayer = null;
+ if (mTonePlayer == null) {
+ Log.w(this, "No player found to stop.");
+ } else {
+ Log.i(this, "Stopping the ringback tone for %s.", call);
+ Log.addEvent(call, STOP_RINGBACK);
+ mTonePlayer.stopTone();
+ mTonePlayer = null;
+ }
}
}
}
+
+ public boolean isRingbackPlaying() {
+ synchronized (mLock) {
+ return mTonePlayer != null;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 16dc5c4..12778b0 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -22,13 +22,18 @@
import static android.provider.Settings.Global.ZEN_MODE_OFF;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Person;
import android.content.Context;
+import android.content.res.Resources;
+import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
+import android.media.Utils;
import android.media.VolumeShaper;
+import android.media.audio.Flags;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -38,13 +43,21 @@
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.os.vibrator.persistence.ParsedVibration;
+import android.os.vibrator.persistence.VibrationXmlParser;
import android.telecom.Log;
import android.telecom.TelecomManager;
+import android.util.Pair;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.LogUtils.EventTimer;
+import com.android.server.telecom.flags.FeatureFlags;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
@@ -59,6 +72,8 @@
*/
@VisibleForTesting
public class Ringer {
+ private static final String TAG = "TelecomRinger";
+
public interface AccessibilityManagerAdapter {
boolean startFlashNotificationSequence(@NonNull Context context,
@AccessibilityManager.FlashNotificationReason int reason);
@@ -84,6 +99,16 @@
// Used for test to notify the completion of RingerAttributes
private CountDownLatch mAttributesLatch;
+ /**
+ * Delay to be used between consecutive vibrations when a non-repeating vibration effect is
+ * provided by the device.
+ *
+ * <p>If looking to customize the loop delay for a device's ring vibration, the desired repeat
+ * behavior should be encoded directly in the effect specification in the device configuration
+ * rather than changing the here (i.e. in `R.raw.default_ringtone_vibration_effect` resource).
+ */
+ private static int DEFAULT_RING_VIBRATION_LOOP_DELAY_MS = 1000;
+
private static final long[] PULSE_PRIMING_PATTERN = {0,12,250,12,500}; // priming + interval
private static final int[] PULSE_PRIMING_AMPLITUDE = {0,255,0,255,0}; // priming + interval
@@ -96,9 +121,11 @@
private static final int[] PULSE_RAMPING_AMPLITUDE = {
77,77,78,79,81,84,87,93,101,114,133,162,205,255,255,0};
- private static final long[] PULSE_PATTERN;
+ @VisibleForTesting
+ public static final long[] PULSE_PATTERN;
- private static final int[] PULSE_AMPLITUDE;
+ @VisibleForTesting
+ public static final int[] PULSE_AMPLITUDE;
private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000;
private static final int RAMPING_RINGER_DURATION = 10000;
@@ -162,6 +189,8 @@
private final InCallController mInCallController;
private final VibrationEffectProxy mVibrationEffectProxy;
private final boolean mIsHapticPlaybackSupportedByDevice;
+ private final FeatureFlags mFlags;
+ private final boolean mRingtoneVibrationSupported;
/**
* For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete
* the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}.
@@ -207,7 +236,8 @@
VibrationEffectProxy vibrationEffectProxy,
InCallController inCallController,
NotificationManager notificationManager,
- AccessibilityManagerAdapter accessibilityManagerAdapter) {
+ AccessibilityManagerAdapter accessibilityManagerAdapter,
+ FeatureFlags featureFlags) {
mLock = new Object();
mSystemSettingsUtil = systemSettingsUtil;
@@ -223,18 +253,17 @@
mNotificationManager = notificationManager;
mAccessibilityManagerAdapter = accessibilityManagerAdapter;
- if (mContext.getResources().getBoolean(R.bool.use_simple_vibration_pattern)) {
- mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
- SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT);
- } else {
- mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(PULSE_PATTERN,
- PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
- }
+ mDefaultVibrationEffect =
+ loadDefaultRingVibrationEffect(
+ mContext, mVibrator, mVibrationEffectProxy, featureFlags);
mIsHapticPlaybackSupportedByDevice =
mSystemSettingsUtil.isHapticPlaybackSupported(mContext);
mAudioManager = mContext.getSystemService(AudioManager.class);
+ mFlags = featureFlags;
+ mRingtoneVibrationSupported = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported);
}
@VisibleForTesting
@@ -329,6 +358,12 @@
mVolumeShaperConfig = null;
+ String vibratorAttrs = String.format("hasVibrator=%b, userRequestsVibrate=%b, "
+ + "ringerMode=%d, isVibratorEnabled=%b",
+ mVibrator.hasVibrator(),
+ mSystemSettingsUtil.isRingVibrationEnabled(mContext),
+ mAudioManager.getRingerMode(), isVibratorEnabled);
+
if (attributes.isRingerAudible()) {
mRingingCall = foregroundCall;
Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);
@@ -375,6 +410,8 @@
}
} else {
foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
+ vibratorAttrs);
return attributes.shouldAcquireAudioFocus(); // ringer not audible
}
}
@@ -391,19 +428,14 @@
isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
}
// Defer ringtone creation to the async player thread.
- Supplier<Ringtone> ringtoneSupplier;
+ Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier = null;
final boolean finalHapticChannelsMuted = hapticChannelsMuted;
- if (isHapticOnly) {
- if (hapticChannelsMuted) {
- Log.i(this,
- "want haptic only ringtone but haptics are muted, skip ringtone play");
- ringtoneSupplier = null;
- } else {
- ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;
- }
- } else {
- ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
+ if (!isHapticOnly) {
+ ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
+ } else if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported) {
+ ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
+ foregroundCall, null, false);
}
// If vibration will be done, reserve the vibrator.
@@ -412,11 +444,7 @@
if (!vibratorReserved) {
foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
- "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, "
- + "isVibratorEnabled=%b",
- mVibrator.hasVibrator(),
- mSystemSettingsUtil.isRingVibrationEnabled(mContext),
- mAudioManager.getRingerMode(), isVibratorEnabled);
+ vibratorAttrs);
}
// The vibration logic depends on the loaded ringtone, but we need to defer the ringtone
@@ -425,9 +453,18 @@
// if the loaded ringtone is null. However if a stop event arrives before the ringtone
// creation finishes, then this consumer can be skipped.
final boolean finalUseCustomVibrationEffect = useCustomVibrationEffect;
- BiConsumer<Ringtone, Boolean> afterRingtoneLogic =
- (Ringtone ringtone, Boolean stopped) -> {
+ BiConsumer<Pair<Uri, Ringtone>, Boolean> afterRingtoneLogic =
+ (Pair<Uri, Ringtone> ringtoneInfo, Boolean stopped) -> {
try {
+ Uri ringtoneUri = null;
+ Ringtone ringtone = null;
+ if (ringtoneInfo != null) {
+ ringtoneUri = ringtoneInfo.first;
+ ringtone = ringtoneInfo.second;
+ } else {
+ Log.w(this, "The ringtone could not be loaded.");
+ }
+
if (stopped.booleanValue() || !vibratorReserved) {
// don't start vibration if the ringing is already abandoned, or the
// vibrator wasn't reserved. This still triggers the mBlockOnRingingFuture.
@@ -438,7 +475,7 @@
if (DEBUG_RINGER) {
Log.d(this, "Using ringtone defined vibration effect.");
}
- vibrationEffect = getVibrationEffectForRingtone(ringtone);
+ vibrationEffect = getVibrationEffectForRingtone(ringtoneUri);
} else {
vibrationEffect = mDefaultVibrationEffect;
}
@@ -446,7 +483,8 @@
boolean isUsingAudioCoupledHaptics =
!finalHapticChannelsMuted && ringtone != null
&& ringtone.hasHapticChannels();
- vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect);
+ vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect,
+ ringtoneUri);
} finally {
// This is used to signal to tests that the async play() call has completed.
if (mBlockOnRingingFuture != null) {
@@ -455,10 +493,10 @@
}
};
deferBlockOnRingingFuture = true; // Run in vibrationLogic.
- if (ringtoneSupplier != null) {
- mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic, isHfpDeviceAttached);
+ if (ringtoneInfoSupplier != null) {
+ mRingtonePlayer.play(ringtoneInfoSupplier, afterRingtoneLogic, isHfpDeviceAttached);
} else {
- afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false);
+ afterRingtoneLogic.accept(/* ringtoneUri, ringtone = */ null, /* stopped= */ false);
}
// shouldAcquireAudioFocus is meant to be true, but that check is deferred to here
@@ -498,13 +536,20 @@
}
private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, Call foregroundCall,
- VibrationEffect effect) {
+ VibrationEffect effect, Uri ringtoneUri) {
if (isUsingAudioCoupledHaptics) {
Log.addEvent(
foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using audio-coupled haptics");
return;
}
+ if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported
+ && Utils.hasVibration(ringtoneUri)) {
+ Log.addEvent(
+ foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using custom haptics");
+ return;
+ }
+
synchronized (mLock) {
// Ensure the reservation is live. The mIsVibrating check should be redundant.
if (foregroundCall == mVibratingCall && !mIsVibrating) {
@@ -515,13 +560,17 @@
mIsVibrating = true;
mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
Log.i(this, "start vibration.");
+ } else {
+ Log.i(this, "vibrateIfNeeded: skip; isVibrating=%b, fgCallId=%s, vibratingCall=%s",
+ mIsVibrating,
+ (foregroundCall == null ? "null" : foregroundCall.getId()),
+ (mVibratingCall == null ? "null" : mVibratingCall.getId()));
}
// else stopped already: this isn't started unless a reservation was made.
}
}
- private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
- Uri ringtoneUri = ringtone.getUri();
+ private VibrationEffect getVibrationEffectForRingtone(Uri ringtoneUri) {
if (ringtoneUri == null) {
return mDefaultVibrationEffect;
}
@@ -547,10 +596,6 @@
}
public void startCallWaiting(Call call, String reason) {
- if (mSystemSettingsUtil.isTheaterModeOn(mContext)) {
- return;
- }
-
if (mInCallController.doesConnectedDialerSupportRinging(
call.getAssociatedUser())) {
Log.addEvent(call, LogUtils.Events.SKIP_RINGING, "Dialer handles");
@@ -570,7 +615,7 @@
Log.addEvent(call, LogUtils.Events.START_CALL_WAITING_TONE, reason);
mCallWaitingCall = call;
mCallWaitingPlayer =
- mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+ mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_CALL_WAITING);
mCallWaitingPlayer.startTone();
}
}
@@ -629,14 +674,21 @@
Log.i(this, "shouldRingForContact: returning computation from DndCallFilter.");
return !call.isCallSuppressedByDoNotDisturb();
}
- final Uri contactUri = call.getHandle();
- final Bundle peopleExtras = new Bundle();
- if (contactUri != null) {
- ArrayList<Person> personList = new ArrayList<>();
- personList.add(new Person.Builder().setUri(contactUri.toString()).build());
- peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
+ Uri contactUri = call.getHandle();
+ if (mFlags.telecomResolveHiddenDependencies()) {
+ if (contactUri == null) {
+ contactUri = Uri.EMPTY;
+ }
+ return mNotificationManager.matchesCallFilter(contactUri);
+ } else {
+ final Bundle peopleExtras = new Bundle();
+ if (contactUri != null) {
+ ArrayList<Person> personList = new ArrayList<>();
+ personList.add(new Person.Builder().setUri(contactUri.toString()).build());
+ peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
+ }
+ return mNotificationManager.matchesCallFilter(peopleExtras);
}
- return mNotificationManager.matchesCallFilter(peopleExtras);
}
private boolean hasExternalRinger(Call foregroundCall) {
@@ -666,7 +718,16 @@
LogUtils.EventTimer timer = new EventTimer();
- boolean isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;
+ boolean isVolumeOverZero;
+
+ if (mFlags.ensureInCarRinging()) {
+ AudioAttributes aa = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();
+ isVolumeOverZero = mAudioManager.shouldNotificationSoundPlay(aa);
+ } else {
+ isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;
+ }
timer.record("isVolumeOverZero");
boolean shouldRingForContact = shouldRingForContact(call);
timer.record("shouldRingForContact");
@@ -686,8 +747,6 @@
boolean hasExternalRinger = hasExternalRinger(call);
timer.record("hasExternalRinger");
// Don't do call waiting operations or vibration unless these are false.
- boolean isTheaterModeOn = mSystemSettingsUtil.isTheaterModeOn(mContext);
- timer.record("isTheaterModeOn");
boolean letDialerHandleRinging = mInCallController.doesConnectedDialerSupportRinging(
call.getAssociatedUser());
timer.record("letDialerHandleRinging");
@@ -696,15 +755,24 @@
timer.record("isWorkProfileInQuietMode");
Log.i(this, "startRinging timings: " + timer);
- boolean endEarly = isTheaterModeOn || letDialerHandleRinging || isSelfManaged ||
- hasExternalRinger || isSilentRingingRequested || isWorkProfileInQuietMode;
+ boolean endEarly =
+ letDialerHandleRinging
+ || isSelfManaged
+ || hasExternalRinger
+ || isSilentRingingRequested
+ || isWorkProfileInQuietMode;
if (endEarly) {
- Log.i(this, "Ending early -- isTheaterModeOn=%s, letDialerHandleRinging=%s, " +
- "isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s, " +
- "isWorkProfileInQuietMode=%s",
- isTheaterModeOn, letDialerHandleRinging, isSelfManaged, hasExternalRinger,
- isSilentRingingRequested, isWorkProfileInQuietMode);
+ Log.i(
+ this,
+ "Ending early -- letDialerHandleRinging=%s, isSelfManaged=%s, "
+ + "hasExternalRinger=%s, silentRingingRequested=%s, "
+ + "isWorkProfileInQuietMode=%s",
+ letDialerHandleRinging,
+ isSelfManaged,
+ hasExternalRinger,
+ isSilentRingingRequested,
+ isWorkProfileInQuietMode);
}
// Acquire audio focus under any of the following conditions:
@@ -757,4 +825,65 @@
return false;
}
}
+
+ @Nullable
+ private static VibrationEffect loadSerializedDefaultRingVibration(
+ Resources resources, Vibrator vibrator) {
+ try {
+ InputStream vibrationInputStream =
+ resources.openRawResource(
+ com.android.internal.R.raw.default_ringtone_vibration_effect);
+ ParsedVibration parsedVibration = VibrationXmlParser
+ .parseDocument(
+ new InputStreamReader(vibrationInputStream, StandardCharsets.UTF_8));
+ if (parsedVibration == null) {
+ Log.w(TAG, "Got null parsed default ring vibration effect.");
+ return null;
+ }
+ return parsedVibration.resolve(vibrator);
+ } catch (IOException | Resources.NotFoundException e) {
+ Log.e(TAG, e, "Error parsing default ring vibration effect.");
+ return null;
+ }
+ }
+
+ private static VibrationEffect loadDefaultRingVibrationEffect(
+ Context context,
+ Vibrator vibrator,
+ VibrationEffectProxy vibrationEffectProxy,
+ FeatureFlags featureFlags) {
+ Resources resources = context.getResources();
+
+ if (resources.getBoolean(R.bool.use_simple_vibration_pattern)) {
+ Log.i(TAG, "Using simple default ring vibration.");
+ return createSimpleRingVibration(vibrationEffectProxy);
+ }
+
+ if (featureFlags.useDeviceProvidedSerializedRingerVibration()) {
+ VibrationEffect parsedEffect = loadSerializedDefaultRingVibration(resources, vibrator);
+ if (parsedEffect != null) {
+ Log.i(TAG, "Using parsed default ring vibration.");
+ // Make the parsed effect repeating to make it vibrate continuously during ring.
+ // If the effect is already repeating, this API call is a no-op.
+ // Otherwise, it uses `DEFAULT_RING_VIBRATION_LOOP_DELAY_MS` when changing a
+ // non-repeating vibration to a repeating vibration.
+ // This is so that we ensure consecutive loops of the vibration play with some gap
+ // in between.
+ return parsedEffect.applyRepeatingIndefinitely(
+ /* wantRepeating= */ true, DEFAULT_RING_VIBRATION_LOOP_DELAY_MS);
+ }
+ // Fallback to the simple vibration if the serialized effect cannot be loaded.
+ return createSimpleRingVibration(vibrationEffectProxy);
+ }
+
+ Log.i(TAG, "Using pulse default ring vibration.");
+ return vibrationEffectProxy.createWaveform(
+ PULSE_PATTERN, PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
+ }
+
+ private static VibrationEffect createSimpleRingVibration(
+ VibrationEffectProxy vibrationEffectProxy) {
+ return vibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
+ SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT);
+ }
}
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 6bcfb4c..c740c24 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -33,7 +33,10 @@
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;
import java.util.List;
@@ -47,24 +50,15 @@
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;
}
- /**
- * Determines if a ringtone has haptic channels.
- * @param ringtone The ringtone URI.
- * @return {@code true} if there is a haptic channel, {@code false} otherwise.
- */
- public boolean hasHapticChannels(Ringtone ringtone) {
- boolean hasHapticChannels = RingtoneManager.hasHapticChannels(ringtone.getUri());
- Log.i(this, "hasHapticChannels %s -> %b", ringtone.getUri(), hasHapticChannels);
- return hasHapticChannels;
- }
-
- public Ringtone getRingtone(Call incomingCall,
+ public Pair<Uri, Ringtone> getRingtone(Call incomingCall,
@Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
// Initializing ringtones on the main thread can deadlock
ThreadUtil.checkNotOnMainThread();
@@ -92,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) {
@@ -106,18 +104,19 @@
}
}
- if (defaultRingtoneUri == null) {
+ ringtoneUri = defaultRingtoneUri;
+ if (ringtoneUri == null) {
return null;
}
try {
ringtone = RingtoneManager.getRingtone(
- contextToUse, defaultRingtoneUri, volumeShaperConfig, audioAttrs);
+ contextToUse, ringtoneUri, volumeShaperConfig, audioAttrs);
} catch (Exception e) {
Log.e(this, e, "getRingtone: exception while getting ringtone.");
}
}
- return ringtone;
+ return new Pair(ringtoneUri, ringtone);
}
private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
@@ -128,41 +127,39 @@
.build();
}
- /** Returns a ringtone to be used when ringer is not audible for the incoming call. */
- @Nullable
- public Ringtone getHapticOnlyRingtone() {
- // Initializing ringtones on the main thread can deadlock
- ThreadUtil.checkNotOnMainThread();
- Uri ringtoneUri = Uri.parse("file://" + mContext.getString(
- com.android.internal.R.string.config_defaultRingtoneVibrationSound));
- AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
- /* hapticChannelsMuted */ false);
- Ringtone ringtone = RingtoneManager.getRingtone(
- mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);
- if (ringtone != null) {
- // Make sure the sound is muted.
- ringtone.setVolume(0);
- }
- return ringtone;
- }
-
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 8fdfb11..1b5c71b 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -54,7 +54,7 @@
/**
* Returns the package name of the app which fills the {@link android.app.role.RoleManager} call
* screening role.
- * @return the package name of the app filling the role, {@code null} otherwise}.
+ * @return the package name of the app filling the role, {@code null} otherwise.
*/
String getDefaultCallScreeningApp(UserHandle userHandle);
@@ -67,9 +67,25 @@
void setTestDefaultCallScreeningApp(String packageName);
/**
+ * Returns the package name of the package which fills the {@link android.app.role.RoleManager}
+ * bt in-call service role.
+ * @return the package name of the package filling the role, {@code null} otherwise.
+ */
+ String[] getBTInCallService();
+
+ /**
+ * Override the {@link android.app.role.RoleManager} bt in-call service package with another
+ * value.
+ * Used for testing purposes only.
+ * @param packageName Package name of the package to fill the bt in-call service role. Where
+ * {@code null}, the override is removed.
+ */
+ void setTestBTInCallService(String packageName);
+
+ /**
* Returns the package name of the app which fills the {@link android.app.role.RoleManager}
* {@link android.app.role.RoleManager#ROLE_DIALER} role.
- * @return the package name of the app filling the role, {@code null} otherwise}.
+ * @return the package name of the app filling the role, {@code null} otherwise.
*/
String getDefaultDialerApp(int user);
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index ac35b3d..55326e8 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -41,6 +41,7 @@
private String mOverrideDefaultCallScreeningApp = null;
private String mOverrideDefaultDialerApp = null;
private List<String> mOverrideCallCompanionApps = new ArrayList<>();
+ private String mOverrideBTInCallService = null;
private Context mContext;
private RoleManager mRoleManager;
private UserHandle mCurrentUserHandle;
@@ -77,6 +78,20 @@
}
@Override
+ public String[] getBTInCallService() {
+ if (mOverrideBTInCallService != null) {
+ return new String [] {mOverrideBTInCallService};
+ }
+ return getBluetoothInCallServicePackageName();
+ }
+
+ @Override
+ public void setTestBTInCallService(String packageName) {
+ mOverrideBTInCallService = packageName;
+ }
+
+
+ @Override
public String getDefaultDialerApp(int user) {
if (mOverrideDefaultDialerApp != null) {
return mOverrideDefaultDialerApp;
@@ -151,6 +166,10 @@
return roleHolders.get(0);
}
+ private String[] getBluetoothInCallServicePackageName() {
+ return mContext.getResources().getStringArray(R.array.system_bluetooth_stack_package_name);
+ }
+
/**
* Returns the application label that corresponds to the given package name
*
@@ -185,8 +204,8 @@
pw.print("(override ");
pw.print(mOverrideDefaultCallRedirectionApp);
pw.print(") ");
- pw.print(getRoleManagerCallRedirectionApp(Binder.getCallingUserHandle()));
}
+ pw.print(getRoleManagerCallRedirectionApp(Binder.getCallingUserHandle()));
pw.println();
pw.print("DefaultCallScreeningApp: ");
@@ -194,19 +213,19 @@
pw.print("(override ");
pw.print(mOverrideDefaultCallScreeningApp);
pw.print(") ");
- pw.print(getRoleManagerCallScreeningApp(Binder.getCallingUserHandle()));
}
+ pw.print(getRoleManagerCallScreeningApp(Binder.getCallingUserHandle()));
pw.println();
pw.print("DefaultCallCompanionApps: ");
- if (mOverrideCallCompanionApps != null) {
+ if (!mOverrideCallCompanionApps.isEmpty()) {
pw.print("(override ");
pw.print(mOverrideCallCompanionApps.stream().collect(Collectors.joining(", ")));
pw.print(") ");
- List<String> appsInRole = getRoleManagerCallCompanionApps();
- if (appsInRole != null) {
- pw.print(appsInRole.stream().collect(Collectors.joining(", ")));
- }
+ }
+ List<String> appsInRole = getRoleManagerCallCompanionApps();
+ if (!appsInRole.isEmpty()) {
+ pw.print(appsInRole.stream().collect(Collectors.joining(", ")));
}
pw.println();
}
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index bf3b488..a18042b 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -29,6 +29,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.Collections;
import java.util.Set;
@@ -240,6 +241,8 @@
* Abbreviated form of the package name from {@link #mComponentName}; used for session logging.
*/
protected final String mPackageAbbreviation;
+ protected final FeatureFlags mFlags;
+
/** The set of callbacks waiting for notification of the binding's success or failure. */
private final Set<BindCallback> mCallbacks = new ArraySet<>();
@@ -282,7 +285,7 @@
* @param userHandle The {@link UserHandle} to use for binding.
*/
protected ServiceBinder(String serviceAction, ComponentName componentName, Context context,
- TelecomSystem.SyncRoot lock, UserHandle userHandle) {
+ TelecomSystem.SyncRoot lock, UserHandle userHandle, FeatureFlags featureFlags) {
Preconditions.checkState(!TextUtils.isEmpty(serviceAction));
Preconditions.checkNotNull(componentName);
@@ -292,6 +295,7 @@
mComponentName = componentName;
mPackageAbbreviation = Log.getPackageAbbreviation(componentName);
mUserHandle = userHandle;
+ mFlags = featureFlags;
}
final UserHandle getUserHandle() {
@@ -305,7 +309,28 @@
}
final void decrementAssociatedCallCount() {
- decrementAssociatedCallCountUpdated();
+ if (mFlags.updatedRcsCallCountTracking()) {
+ decrementAssociatedCallCountUpdated();
+ } else {
+ decrementAssociatedCallCount(false /*isSuppressingUnbind*/);
+ }
+ }
+
+ final void decrementAssociatedCallCount(boolean isSuppressingUnbind) {
+ // This is the legacy method - will be removed after the Flags.updatedRcsCallCountTracking
+ // mendel study completes.
+ if (mAssociatedCallCount > 0) {
+ mAssociatedCallCount--;
+ Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount,
+ mComponentName.flattenToShortString());
+
+ if (!isSuppressingUnbind && mAssociatedCallCount == 0) {
+ unbind();
+ }
+ } else {
+ Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero",
+ mComponentName.getClassName());
+ }
}
final void decrementAssociatedCallCountUpdated() {
diff --git a/src/com/android/server/telecom/SystemSettingsUtil.java b/src/com/android/server/telecom/SystemSettingsUtil.java
index cdd14df..d846cce 100644
--- a/src/com/android/server/telecom/SystemSettingsUtil.java
+++ b/src/com/android/server/telecom/SystemSettingsUtil.java
@@ -35,11 +35,6 @@
private static final String RAMPING_RINGER_AUDIO_COUPLED_VIBRATION_ENABLED =
"ramping_ringer_audio_coupled_vibration_enabled";
- public boolean isTheaterModeOn(Context context) {
- return Settings.Global.getInt(context.getContentResolver(), Settings.Global.THEATER_MODE_ON,
- 0) == 1;
- }
-
public boolean isRingVibrationEnabled(Context context) {
// VIBRATE_WHEN_RINGING setting was deprecated, only RING_VIBRATION_INTENSITY controls the
// ringtone vibrations on/off state now. Ramping ringer should only be applied when ring
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 7d3eeb6..488524f 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -53,13 +53,16 @@
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.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;
@@ -79,17 +82,23 @@
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.voip.IncomingCallTransaction;
-import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
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;
@@ -101,45 +110,6 @@
*/
public class TelecomServiceImpl {
- public interface SubscriptionManagerAdapter {
- int getDefaultVoiceSubId();
- }
-
- static class SubscriptionManagerAdapterImpl implements SubscriptionManagerAdapter {
- @Override
- public int getDefaultVoiceSubId() {
- return SubscriptionManager.getDefaultVoiceSubscriptionId();
- }
- }
-
- public interface SettingsSecureAdapter {
- void putStringForUser(ContentResolver resolver, String name, String value, int userHandle);
-
- String getStringForUser(ContentResolver resolver, String name, int userHandle);
- }
-
- static class SettingsSecureAdapterImpl implements SettingsSecureAdapter {
- @Override
- public void putStringForUser(ContentResolver resolver, String name, String value,
- int userHandle) {
- Settings.Secure.putStringForUser(resolver, name, value, userHandle);
- }
-
- @Override
- public String getStringForUser(ContentResolver resolver, String name, int userHandle) {
- return Settings.Secure.getStringForUser(resolver, name, userHandle);
- }
- }
-
- private static final String TAG = "TelecomServiceImpl";
- private static final String TIME_LINE_ARG = "timeline";
- private static final int DEFAULT_VIDEO_STATE = -1;
- private static final String PERMISSION_HANDLE_CALL_INTENT =
- "android.permission.HANDLE_CALL_INTENT";
- private static final String ADD_CALL_ERR_MSG = "Call could not be created or found. "
- + "Retry operation.";
- private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
-
/**
* Anomaly Report UUIDs and corresponding error descriptions specific to TelecomServiceImpl.
*/
@@ -171,17 +141,39 @@
UUID.fromString("4edf6c8d-1e43-4c94-b0fc-a40c8d80cfe8");
public static final String PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG =
"Security exception thrown while placing an outgoing call.";
-
- @VisibleForTesting
- public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
- mAnomalyReporter = mAnomalyReporterAdapter;
- }
-
+ private static final String TAG = "TelecomServiceImpl";
+ private static final String TIME_LINE_ARG = "timeline";
+ private static final int DEFAULT_VIDEO_STATE = -1;
+ private static final String PERMISSION_HANDLE_CALL_INTENT =
+ "android.permission.HANDLE_CALL_INTENT";
+ private static final String ADD_CALL_ERR_MSG = "Call could not be created or found. "
+ + "Retry operation.";
+ private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+ private final CallIntentProcessor.Adapter mCallIntentProcessorAdapter;
+ private final UserCallIntentProcessorFactory mUserCallIntentProcessorFactory;
+ private final DefaultDialerCache mDefaultDialerCache;
+ private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
+ private final SettingsSecureAdapter mSettingsSecureAdapter;
+ private final TelecomSystem.SyncRoot mLock;
+ private final TransactionalServiceRepository mTransactionalServiceRepository;
+ private final BlockedNumbersManager mBlockedNumbersManager;
+ private final FeatureFlags mFeatureFlags;
+ private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+ private final TelecomMetricsController mMetricsController;
+ private final String mSystemUiPackageName;
+ private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+ private final Context mContext;
+ private final AppOpsManager mAppOpsManager;
+ private final PackageManager mPackageManager;
+ private final CallsManager mCallsManager;
+ private TransactionManager mTransactionManager;
private final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {
@Override
public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
String callId, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aC", Log.getPackageAbbreviation(callingPackage));
Log.i(TAG, "addCall: id=[%s], attributes=[%s]", callId, callAttributes);
@@ -194,21 +186,23 @@
enforcePhoneAccountIsRegisteredEnabled(handle, handle.getUserHandle());
enforceCallingPackage(callingPackage, "addCall");
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+
// add extras about info used for FGS delegation
Bundle extras = new Bundle();
extras.putInt(CallAttributes.CALLER_UID_KEY, Binder.getCallingUid());
extras.putInt(CallAttributes.CALLER_PID_KEY, Binder.getCallingPid());
- VoipCallTransaction transaction = null;
+ CallTransaction transaction = null;
// create transaction based on the call direction
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. "
@@ -219,7 +213,7 @@
mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
+ public void onResult(CallTransactionResult result) {
Log.d(TAG, "addCall: onResult");
Call call = result.getCall();
@@ -236,6 +230,10 @@
callEventCallback, mCallsManager, call);
call.setTransactionServiceWrapper(serviceWrapper);
+
+ if (mFeatureFlags.transactionalVideoState()) {
+ call.setTransactionalCallSupportsVideoCalling(callAttributes);
+ }
ICallControl clientCallControl = serviceWrapper.getICallControl();
if (clientCallControl == null) {
@@ -253,7 +251,9 @@
onAddCallControl(callId, callEventCallback, null, exception);
}
});
+ event.setResult(ApiStats.RESULT_NORMAL);
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -277,12 +277,17 @@
@Override
public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme,
String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETDEFAULTOUTGOINGPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gDOPA", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
PhoneAccountHandle phoneAccountHandle = null;
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+
+ event.setResult(ApiStats.RESULT_EXCEPTION);
try {
phoneAccountHandle = mPhoneAccountRegistrar
.getOutgoingPhoneAccountForScheme(uriScheme, callingUserHandle);
@@ -292,6 +297,8 @@
} finally {
Binder.restoreCallingIdentity(token);
}
+
+ event.setResult(ApiStats.RESULT_NORMAL);
if (isCallerSimCallManager(phoneAccountHandle)
|| canReadPhoneState(
callingPackage,
@@ -302,12 +309,16 @@
return null;
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@Override
public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
synchronized (mLock) {
try {
Log.startSession("TSI.gUSOPA", Log.getPackageAbbreviation(callingPackage));
@@ -315,6 +326,7 @@
throw new SecurityException("Only the default dialer, or caller with "
+ "READ_PRIVILEGED_PHONE_STATE can call this method.");
}
+ event.setResult(ApiStats.RESULT_NORMAL);
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
return mPhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount(
callingUserHandle);
@@ -322,6 +334,7 @@
Log.e(this, e, "getUserSelectedOutgoingPhoneAccount");
throw e;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -329,6 +342,9 @@
@Override
public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.sUSOPA");
synchronized (mLock) {
@@ -338,6 +354,7 @@
try {
mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(
accountHandle, callingUserHandle);
+ event.setResult(ApiStats.RESULT_NORMAL);
} catch (Exception e) {
Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
mAnomalyReporter.reportAnomaly(SET_USER_PHONE_ACCOUNT_ERROR_UUID,
@@ -348,15 +365,38 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@Override
public ParceledListSlice<PhoneAccountHandle> getCallCapablePhoneAccounts(
- boolean includeDisabledAccounts, String callingPackage, String callingFeatureId) {
+ boolean includeDisabledAccounts, String callingPackage,
+ String callingFeatureId, boolean acrossProfiles) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETCALLCAPABLEPHONEACCOUNTS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gCCPA", Log.getPackageAbbreviation(callingPackage));
+
+ if (mTelephonyFeatureFlags.workProfileApiSplit()) {
+ if (acrossProfiles) {
+ enforceInAppCrossProfilePermission();
+ }
+
+ if (includeDisabledAccounts && !canReadPrivilegedPhoneState(
+ callingPackage, "getCallCapablePhoneAccounts")) {
+ throw new SecurityException(
+ "Requires READ_PRIVILEGED_PHONE_STATE permission.");
+ }
+
+ if (!includeDisabledAccounts && !canReadPhoneState(callingPackage,
+ callingFeatureId, "Requires READ_PHONE_STATE permission.")) {
+ throw new SecurityException("Requires READ_PHONE_STATE permission.");
+ }
+ }
+
if (includeDisabledAccounts &&
!canReadPrivilegedPhoneState(
callingPackage, "getCallCapablePhoneAccounts")) {
@@ -366,9 +406,13 @@
"getCallCapablePhoneAccounts")) {
return ParceledListSlice.emptyList();
}
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
- boolean crossUserAccess = hasInAppCrossUserPermission();
+ boolean crossUserAccess = (!mTelephonyFeatureFlags.workProfileApiSplit()
+ || acrossProfiles) && (mTelephonyFeatureFlags.workProfileApiSplit()
+ ? hasInAppCrossProfilePermission()
+ : hasInAppCrossUserPermission());
long token = Binder.clearCallingIdentity();
try {
return new ParceledListSlice<>(
@@ -376,6 +420,7 @@
includeDisabledAccounts, callingUserHandle,
crossUserAccess));
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getCallCapablePhoneAccounts");
mAnomalyReporter.reportAnomaly(GET_CALL_CAPABLE_ACCOUNTS_ERROR_UUID,
GET_CALL_CAPABLE_ACCOUNTS_ERROR_MSG);
@@ -385,6 +430,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -392,6 +438,9 @@
@Override
public ParceledListSlice<PhoneAccountHandle> getSelfManagedPhoneAccounts(
String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETSELFMANAGEDPHONEACCOUNTS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gSMPA", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId,
@@ -401,10 +450,12 @@
synchronized (mLock) {
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
.getSelfManagedPhoneAccounts(callingUserHandle));
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getSelfManagedPhoneAccounts");
throw e;
} finally {
@@ -412,6 +463,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -419,6 +471,9 @@
@Override
public ParceledListSlice<PhoneAccountHandle> getOwnSelfManagedPhoneAccounts(
String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETOWNSELFMANAGEDPHONEACCOUNTS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gOSMPA", Log.getPackageAbbreviation(callingPackage));
try {
@@ -434,11 +489,13 @@
synchronized (mLock) {
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
.getSelfManagedPhoneAccountsForPackage(callingPackage,
callingUserHandle));
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e,
"getSelfManagedPhoneAccountsForPackage");
throw e;
@@ -447,6 +504,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -454,6 +512,9 @@
@Override
public ParceledListSlice<PhoneAccountHandle> getPhoneAccountsSupportingScheme(
String uriScheme, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETPHONEACCOUNTSSUPPORTINGSCHEME,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gPASS", Log.getPackageAbbreviation(callingPackage));
try {
@@ -468,11 +529,13 @@
synchronized (mLock) {
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
- .getCallCapablePhoneAccounts(uriScheme, false,
- callingUserHandle, false));
+ .getCallCapablePhoneAccounts(uriScheme, false,
+ callingUserHandle, false));
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme);
throw e;
} finally {
@@ -480,6 +543,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -488,42 +552,53 @@
public ParceledListSlice<PhoneAccountHandle> getPhoneAccountsForPackage(
String packageName) {
//TODO: Deprecate this in S
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETPHONEACCOUNTSFORPACKAGE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
- enforceCallingPackage(packageName, "getPhoneAccountsForPackage");
- } catch (SecurityException se1) {
- EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
- "getPhoneAccountsForPackage: invalid calling package");
- throw se1;
- }
-
- try {
- enforcePermission(READ_PRIVILEGED_PHONE_STATE);
- } catch (SecurityException se2) {
- EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
- "getPhoneAccountsForPackage: no permission");
- throw se2;
- }
-
- synchronized (mLock) {
- final UserHandle callingUserHandle = Binder.getCallingUserHandle();
- long token = Binder.clearCallingIdentity();
try {
- Log.startSession("TSI.gPAFP");
- return new ParceledListSlice<>(mPhoneAccountRegistrar
- .getAllPhoneAccountHandlesForPackage(callingUserHandle, packageName));
- } catch (Exception e) {
- Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
- Log.endSession();
+ enforceCallingPackage(packageName, "getPhoneAccountsForPackage");
+ } catch (SecurityException se1) {
+ EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
+ "getPhoneAccountsForPackage: invalid calling package");
+ throw se1;
}
+
+ try {
+ enforcePermission(READ_PRIVILEGED_PHONE_STATE);
+ } catch (SecurityException se2) {
+ EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
+ "getPhoneAccountsForPackage: no permission");
+ throw se2;
+ }
+
+ synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
+ try {
+ Log.startSession("TSI.gPAFP");
+ return new ParceledListSlice<>(mPhoneAccountRegistrar
+ .getAllPhoneAccountHandlesForPackage(
+ callingUserHandle, packageName));
+ } catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+ Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
+ }
+ }
+ } finally {
+ logEvent(event);
}
}
@Override
public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gPA", Log.getPackageAbbreviation(callingPackage));
try {
@@ -550,6 +625,7 @@
Set<String> permissions = computePermissionsForBoundPackage(
Set.of(MODIFY_PHONE_STATE), null);
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
// In ideal case, we should not resolve the handle across profiles. But
// given the fact that profile's call is handled by its parent user's
@@ -560,6 +636,7 @@
/* acrossProfiles */ true);
return maybeCleansePhoneAccount(account, permissions);
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getPhoneAccount %s", accountHandle);
mAnomalyReporter.reportAnomaly(GET_PHONE_ACCOUNT_ERROR_UUID,
GET_PHONE_ACCOUNT_ERROR_MSG);
@@ -569,14 +646,70 @@
}
}
} finally {
+ logEvent(event);
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public ParceledListSlice<PhoneAccount> getRegisteredPhoneAccounts(String callingPackage,
+ String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETREGISTEREDPHONEACCOUNTS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
+ try {
+ Log.startSession("TSI.gRPA", Log.getPackageAbbreviation(callingPackage));
+ try {
+ enforceCallingPackage(callingPackage, "getRegisteredPhoneAccounts");
+ } catch (SecurityException se) {
+ EventLog.writeEvent(0x534e4554, "307609763", Binder.getCallingUid(),
+ "getRegisteredPhoneAccounts: invalid calling package");
+ throw se;
+ }
+
+ boolean hasCrossUserAccess = false;
+ try {
+ enforceInAppCrossUserPermission();
+ hasCrossUserAccess = true;
+ } catch (SecurityException e) {
+ // pass through
+ }
+
+ synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
+ try {
+ return new ParceledListSlice<>(
+ mPhoneAccountRegistrar.getPhoneAccounts(
+ 0 /* capabilities */,
+ 0 /* excludedCapabilities */,
+ null /* UriScheme */,
+ callingPackage,
+ true /* includeDisabledAccounts */,
+ callingUserHandle,
+ hasCrossUserAccess /* crossUserAccess */,
+ false /* includeAll */));
+ } catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+ Log.e(this, e, "getRegisteredPhoneAccounts");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } finally {
+ logEvent(event);
Log.endSession();
}
}
@Override
public int getAllPhoneAccountsCount() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETALLPHONEACCOUNTSCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gAPAC");
+ event.setCallerUid(Binder.getCallingUid());
try {
enforceModifyPermission(
"getAllPhoneAccountsCount requires MODIFY_PHONE_STATE permission.");
@@ -587,22 +720,27 @@
}
synchronized (mLock) {
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
// This list is pre-filtered for the calling user.
return getAllPhoneAccounts().getList().size();
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getAllPhoneAccountsCount");
throw e;
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@Override
public ParceledListSlice<PhoneAccount> getAllPhoneAccounts() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETALLPHONEACCOUNTS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
synchronized (mLock) {
try {
Log.startSession("TSI.gAPA");
@@ -617,16 +755,19 @@
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
.getAllPhoneAccounts(callingUserHandle, false));
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getAllPhoneAccounts");
throw e;
} finally {
Binder.restoreCallingIdentity(token);
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -634,8 +775,11 @@
@Override
public ParceledListSlice<PhoneAccountHandle> getAllPhoneAccountHandles() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETALLPHONEACCOUNTHANDLES,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gAPAH");
+
try {
enforceModifyPermission(
"getAllPhoneAccountHandles requires MODIFY_PHONE_STATE permission.");
@@ -649,24 +793,29 @@
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
boolean crossUserAccess = hasInAppCrossUserPermission();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
.getAllPhoneAccountHandles(callingUserHandle,
crossUserAccess));
} catch (Exception e) {
- Log.e(this, e, "getAllPhoneAccounts");
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+ Log.e(this, e, "getAllPhoneAccountsHandles");
throw e;
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@Override
public PhoneAccountHandle getSimCallManager(int subId, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETSIMCALLMANAGER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
synchronized (mLock) {
try {
Log.startSession("TSI.gSCM", Log.getPackageAbbreviation(callingPackage));
@@ -677,6 +826,7 @@
if (user != ActivityManager.getCurrentUser()) {
enforceCrossUserPermission(callingUid);
}
+ event.setResult(ApiStats.RESULT_NORMAL);
return mPhoneAccountRegistrar.getSimCallManager(subId, UserHandle.of(user));
} finally {
Binder.restoreCallingIdentity(token);
@@ -687,6 +837,7 @@
GET_SIM_MANAGER_ERROR_MSG);
throw e;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -694,6 +845,8 @@
@Override
public PhoneAccountHandle getSimCallManagerForUser(int user, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETSIMCALLMANAGERFORUSER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
synchronized (mLock) {
try {
Log.startSession("TSI.gSCMFU", Log.getPackageAbbreviation(callingPackage));
@@ -702,6 +855,7 @@
enforceCrossUserPermission(callingUid);
}
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return mPhoneAccountRegistrar.getSimCallManager(UserHandle.of(user));
} finally {
@@ -713,6 +867,7 @@
GET_SIM_MANAGER_FOR_USER_ERROR_MSG);
throw e;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -720,22 +875,27 @@
@Override
public void registerPhoneAccount(PhoneAccount account, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_REGISTERPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.rPA", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
try {
enforcePhoneAccountModificationForPackage(
account.getAccountHandle().getComponentName().getPackageName());
- if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ || (mFeatureFlags.enforceTransactionalExclusivity()
+ && account.hasCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS))) {
enforceRegisterSelfManaged();
if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) ||
account.hasCapabilities(
PhoneAccount.CAPABILITY_CONNECTION_MANAGER) ||
account.hasCapabilities(
PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
- throw new SecurityException("Self-managed ConnectionServices " +
- "cannot also be call capable, connection managers, or " +
- "SIM accounts.");
+ throw new SecurityException("Self-managed ConnectionServices "
+ + "cannot also be call capable, connection managers, or "
+ + "SIM accounts.");
}
// For self-managed CS, the phone account registrar will override the
@@ -760,6 +920,9 @@
Bundle extras = account.getExtras();
if (extras != null
&& extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+ // System apps should be granted the MODIFY_PHONE_STATE permission.
+ enforceModifyPermission(
+ "registerPhoneAccount requires MODIFY_PHONE_STATE permission.");
enforceRegisterSkipCallFiltering();
}
final int callingUid = Binder.getCallingUid();
@@ -781,7 +944,15 @@
// Validate the profile boundary of the given image URI.
validateAccountIconUserBoundary(account.getIcon());
+ if (mTelephonyFeatureFlags.simultaneousCallingIndications()
+ && account.hasSimultaneousCallingRestriction()) {
+ validateSimultaneousCallingPackageNames(
+ account.getAccountHandle().getComponentName().getPackageName(),
+ account.getSimultaneousCallingRestriction());
+ }
+
final long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
Log.i(this, "registerPhoneAccount: account=%s",
account);
@@ -797,6 +968,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -804,6 +976,8 @@
@Override
public void unregisterPhoneAccount(PhoneAccountHandle accountHandle,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_UNREGISTERPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
synchronized (mLock) {
try {
Log.startSession("TSI.uPA", Log.getPackageAbbreviation(callingPackage));
@@ -811,6 +985,7 @@
accountHandle.getComponentName().getPackageName());
enforceUserHandleMatchesCaller(accountHandle);
final long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
mPhoneAccountRegistrar.unregisterPhoneAccount(accountHandle);
} finally {
@@ -820,6 +995,7 @@
Log.e(this, e, "unregisterPhoneAccount %s", accountHandle);
throw e;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -827,16 +1003,20 @@
@Override
public void clearAccounts(String packageName) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_CLEARACCOUNTS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
synchronized (mLock) {
try {
Log.startSession("TSI.cA");
enforcePhoneAccountModificationForPackage(packageName);
+ event.setResult(ApiStats.RESULT_NORMAL);
mPhoneAccountRegistrar
.clearAccounts(packageName, Binder.getCallingUserHandle());
} catch (Exception e) {
Log.e(this, e, "clearAccounts %s", packageName);
throw e;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -848,6 +1028,8 @@
@Override
public boolean isVoiceMailNumber(PhoneAccountHandle accountHandle, String number,
String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISVOICEMAILNUMBER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.iVMN", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
@@ -861,9 +1043,11 @@
return false;
}
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return mPhoneAccountRegistrar.isVoiceMailNumber(accountHandle, number);
} catch (Exception e) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.e(this, e, "getSubscriptionIdForPhoneAccount");
throw e;
} finally {
@@ -871,6 +1055,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -881,6 +1066,8 @@
@Override
public String getVoiceMailNumber(PhoneAccountHandle accountHandle, String callingPackage,
String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETVOICEMAILNUMBER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gVMN", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "getVoiceMailNumber")) {
@@ -901,12 +1088,18 @@
.getSubscriptionIdForPhoneAccount(accountHandle);
}
}
+ event.setResult(ApiStats.RESULT_NORMAL);
return getTelephonyManager(subId).getVoiceMailNumber();
+ } catch (UnsupportedOperationException ignored) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+ Log.w(this, "getVoiceMailNumber: no Telephony");
+ return null;
} catch (Exception e) {
Log.e(this, e, "getSubscriptionIdForPhoneAccount");
throw e;
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -917,6 +1110,8 @@
@Override
public String getLine1Number(PhoneAccountHandle accountHandle, String callingPackage,
String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETLINE1NUMBER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("getL1N", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneNumbers(callingPackage, callingFeatureId, "getLine1Number")) {
@@ -937,7 +1132,12 @@
subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
accountHandle);
}
+ event.setResult(ApiStats.RESULT_NORMAL);
return getTelephonyManager(subId).getLine1Number();
+ } catch (UnsupportedOperationException ignored) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+ Log.w(this, "getLine1Number: no telephony");
+ return null;
} catch (Exception e) {
Log.e(this, e, "getSubscriptionIdForPhoneAccount");
throw e;
@@ -945,6 +1145,7 @@
Binder.restoreCallingIdentity(token);
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -954,6 +1155,8 @@
*/
@Override
public void silenceRinger(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_SILENCERINGER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.sR", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
@@ -961,17 +1164,20 @@
UserHandle callingUserHandle = Binder.getCallingUserHandle();
boolean crossUserAccess = hasInAppCrossUserPermission();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_EXCEPTION);
try {
Log.i(this, "Silence Ringer requested by %s", callingPackage);
Set<UserHandle> userHandles = mCallsManager.getCallAudioManager().
silenceRingers(mContext, callingUserHandle,
crossUserAccess);
+ event.setResult(ApiStats.RESULT_NORMAL);
mCallsManager.getInCallController().silenceRinger(userHandles);
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -983,10 +1189,13 @@
*/
@Override
public ComponentName getDefaultPhoneApp() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETDEFAULTPHONEAPP,
+ Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
try {
Log.startSession("TSI.gDPA");
return mDefaultDialerCache.getDialtactsSystemDialerComponent();
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -999,6 +1208,8 @@
*/
@Override
public String getDefaultDialerPackage(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETDEFAULTDIALERPACKAGE,
+ Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
try {
Log.startSession("TSI.gDDP", Log.getPackageAbbreviation(callingPackage));
int callerUserId = UserHandle.getCallingUserId();
@@ -1010,6 +1221,7 @@
Binder.restoreCallingIdentity(token);
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1023,18 +1235,23 @@
*/
@Override
public String getDefaultDialerPackageForUser(int userId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_GETDEFAULTDIALERPACKAGEFORUSER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gDDPU");
mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
"READ_PRIVILEGED_PHONE_STATE permission required.");
final long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return mDefaultDialerCache.getDefaultDialerApplication(userId);
} finally {
Binder.restoreCallingIdentity(token);
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1044,10 +1261,13 @@
*/
@Override
public String getSystemDialerPackage(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETSYSTEMDIALERPACKAGE,
+ Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
try {
Log.startSession("TSI.gSDP", Log.getPackageAbbreviation(callingPackage));
return mDefaultDialerCache.getSystemDialerApplication();
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1075,17 +1295,20 @@
*/
@Override
public boolean isInCall(String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.iIC", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "isInCall")) {
return false;
}
-
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
return mCallsManager.hasOngoingCalls(Binder.getCallingUserHandle(),
hasInAppCrossUserPermission());
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1095,9 +1318,13 @@
*/
@Override
public boolean hasManageOngoingCallsPermission(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_HASMANAGEONGOINGCALLSPERMISSION,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.hMOCP", Log.getPackageAbbreviation(callingPackage));
enforceCallingPackage(callingPackage, "hasManageOngoingCallsPermission");
+ event.setResult(ApiStats.RESULT_NORMAL);
return PermissionChecker.checkPermissionForDataDeliveryFromDataSource(
mContext, Manifest.permission.MANAGE_ONGOING_CALLS,
Binder.getCallingPid(),
@@ -1107,6 +1334,7 @@
"Checking whether the caller has MANAGE_ONGOING_CALLS permission")
== PermissionChecker.PERMISSION_GRANTED;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1116,18 +1344,21 @@
*/
@Override
public boolean isInManagedCall(String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINMANAGEDCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.iIMC", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "isInManagedCall")) {
throw new SecurityException("Only the default dialer or caller with " +
"READ_PHONE_STATE permission can use this method.");
}
-
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
return mCallsManager.hasOngoingManagedCalls(Binder.getCallingUserHandle(),
hasInAppCrossUserPermission());
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1137,6 +1368,8 @@
*/
@Override
public boolean isRinging(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISRINGING,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.iR");
if (!isPrivilegedDialerCalling(callingPackage)) {
@@ -1149,6 +1382,7 @@
}
}
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
// Note: We are explicitly checking the calls telecom is tracking rather than
// relying on mCallsManager#getCallState(). Since getCallState() relies on the
@@ -1158,6 +1392,7 @@
return mCallsManager.hasRingingOrSimulatedRingingCall();
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1170,6 +1405,8 @@
@Deprecated
@Override
public int getCallState() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETCALLSTATE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.getCallState(DEPRECATED)");
if (CompatChanges.isChangeEnabled(
@@ -1180,6 +1417,7 @@
throw new SecurityException("This method can only be used for applications "
+ "targeting API version 30 or less.");
}
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
return mCallsManager.getCallState();
}
@@ -1193,12 +1431,14 @@
*/
@Override
public int getCallStateUsingPackage(String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETCALLSTATEUSINGPACKAGE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.getCallStateUsingPackage");
// ensure the callingPackage is not spoofed
// skip check for privileged UIDs and throw SE if package does not match records
- if (!isPrivilegedUid(callingPackage)
+ if (!isPrivilegedUid()
&& !callingUidMatchesPackageManagerRecords(callingPackage)) {
EventLog.writeEvent(0x534e4554, "236813210", Binder.getCallingUid(),
"getCallStateUsingPackage");
@@ -1221,25 +1461,46 @@
+ " for API version 31+");
}
}
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
return mCallsManager.getCallState();
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
- private boolean isPrivilegedUid(String callingPackage) {
+ private boolean isPrivilegedUid() {
int callingUid = Binder.getCallingUid();
- boolean isPrivileged = false;
- switch (callingUid) {
- case Process.ROOT_UID:
- case Process.SYSTEM_UID:
- case Process.SHELL_UID:
- isPrivileged = true;
- break;
+ return mFeatureFlags.allowSystemAppsResolveVoipCalls()
+ ? (UserHandle.isSameApp(callingUid, Process.ROOT_UID)
+ || UserHandle.isSameApp(callingUid, Process.SYSTEM_UID)
+ || UserHandle.isSameApp(callingUid, Process.SHELL_UID))
+ : (callingUid == Process.ROOT_UID
+ || callingUid == Process.SYSTEM_UID
+ || callingUid == Process.SHELL_UID);
+ }
+
+ private boolean isSysUiUid() {
+ int callingUid = Binder.getCallingUid();
+ int systemUiUid;
+ if (mPackageManager != null && mSystemUiPackageName != null) {
+ try {
+ systemUiUid = mPackageManager.getPackageUid(mSystemUiPackageName, 0);
+ Log.i(TAG, "isSysUiUid: callingUid = " + callingUid + "; systemUiUid = "
+ + systemUiUid);
+ return UserHandle.isSameApp(callingUid, systemUiUid);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "isSysUiUid: caught PackageManager NameNotFoundException = " + e);
+ return false;
+ }
+ } else {
+ Log.w(TAG, "isSysUiUid: caught null check and returned false; "
+ + "mPackageManager = " + mPackageManager + "; mSystemUiPackageName = "
+ + mSystemUiPackageName);
}
- return isPrivileged;
+ return false;
}
/**
@@ -1247,21 +1508,32 @@
*/
@Override
public boolean endCall(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ENDCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.eC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
if (!enforceAnswerCallPermission(callingPackage, Binder.getCallingUid())) {
throw new SecurityException("requires ANSWER_PHONE_CALLS permission");
}
-
+ // Legacy behavior is to ignore whether the invocation is from a system app:
+ boolean isCallerPrivileged = false;
+ if (mFeatureFlags.allowSystemAppsResolveVoipCalls()) {
+ isCallerPrivileged = isPrivilegedUid() || isSysUiUid();
+ Log.i(TAG, "endCall: Binder.getCallingUid = [" +
+ Binder.getCallingUid() + "] isCallerPrivileged = " +
+ isCallerPrivileged);
+ }
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
- return endCallInternal(callingPackage);
+ return endCallInternal(callingPackage, isCallerPrivileged);
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1271,19 +1543,31 @@
*/
@Override
public void acceptRingingCall(String packageName) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ACCEPTRINGINGCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aRC", Log.getPackageAbbreviation(packageName));
synchronized (mLock) {
if (!enforceAnswerCallPermission(packageName, Binder.getCallingUid())) return;
-
+ // Legacy behavior is to ignore whether the invocation is from a system app:
+ boolean isCallerPrivileged = false;
+ if (mFeatureFlags.allowSystemAppsResolveVoipCalls()) {
+ isCallerPrivileged = isPrivilegedUid() || isSysUiUid();
+ Log.i(TAG, "acceptRingingCall: Binder.getCallingUid = [" +
+ Binder.getCallingUid() + "] isCallerPrivileged = " +
+ isCallerPrivileged);
+ }
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
- acceptRingingCallInternal(DEFAULT_VIDEO_STATE, packageName);
+ acceptRingingCallInternal(DEFAULT_VIDEO_STATE, packageName,
+ isCallerPrivileged);
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1293,19 +1577,31 @@
*/
@Override
public void acceptRingingCallWithVideoState(String packageName, int videoState) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_ACCEPTRINGINGCALLWITHVIDEOSTATE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aRCWVS", Log.getPackageAbbreviation(packageName));
synchronized (mLock) {
if (!enforceAnswerCallPermission(packageName, Binder.getCallingUid())) return;
-
+ // Legacy behavior is to ignore whether the invocation is from a system app:
+ boolean isCallerPrivileged = false;
+ if (mFeatureFlags.allowSystemAppsResolveVoipCalls()) {
+ isCallerPrivileged = isPrivilegedUid() || isSysUiUid();
+ Log.i(TAG, "acceptRingingCallWithVideoState: Binder.getCallingUid = "
+ + "[" + Binder.getCallingUid() + "] isCallerPrivileged = " +
+ isCallerPrivileged);
+ }
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
- acceptRingingCallInternal(videoState, packageName);
+ acceptRingingCallInternal(videoState, packageName, isCallerPrivileged);
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1316,6 +1612,8 @@
@Override
public void showInCallScreen(boolean showDialpad, String callingPackage,
String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_SHOWINCALLSCREEN,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.sICS", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "showInCallScreen")) {
@@ -1323,16 +1621,18 @@
}
synchronized (mLock) {
-
UserHandle callingUser = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
- mCallsManager.getInCallController().bringToForeground(showDialpad, callingUser);
+ mCallsManager.getInCallController().bringToForeground(
+ showDialpad, callingUser);
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1342,12 +1642,16 @@
*/
@Override
public void cancelMissedCallsNotification(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_CANCELMISSEDCALLSNOTIFICATION,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.cMCN", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
UserHandle userHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
mCallsManager.getMissedCallNotifier().clearMissedCalls(userHandle);
} finally {
@@ -1355,6 +1659,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1364,6 +1669,8 @@
*/
@Override
public boolean handlePinMmi(String dialString, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_HANDLEPINMMI,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.hPM", Log.getPackageAbbreviation(callingPackage));
enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
@@ -1371,6 +1678,7 @@
// Switch identity so that TelephonyManager checks Telecom's permissions
// instead.
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
boolean retval = false;
try {
retval = getTelephonyManager(
@@ -1382,6 +1690,7 @@
return retval;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1392,9 +1701,11 @@
@Override
public boolean handlePinMmiForPhoneAccount(PhoneAccountHandle accountHandle,
String dialString, String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_HANDLEPINMMIFORPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.hPMFPA", Log.getPackageAbbreviation(callingPackage));
-
enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
UserHandle callingUserHandle = Binder.getCallingUserHandle();
synchronized (mLock) {
@@ -1409,6 +1720,7 @@
// Switch identity so that TelephonyManager checks Telecom's permissions
// instead.
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
boolean retval = false;
int subId;
try {
@@ -1416,13 +1728,20 @@
subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
accountHandle);
}
- retval = getTelephonyManager(subId)
- .handlePinMmiForSubscriber(subId, dialString);
+ try {
+ retval = getTelephonyManager(subId)
+ .handlePinMmiForSubscriber(subId, dialString);
+ } catch (UnsupportedOperationException uoe) {
+ event.setResult(ApiStats.RESULT_EXCEPTION);
+ Log.w(this, "handlePinMmiForPhoneAccount: no telephony");
+ retval = false;
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
return retval;
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1433,6 +1752,8 @@
@Override
public Uri getAdnUriForPhoneAccount(PhoneAccountHandle accountHandle,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETADNURIFORPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aAUFPA", Log.getPackageAbbreviation(callingPackage));
enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
@@ -1447,6 +1768,7 @@
// Switch identity so that TelephonyManager checks Telecom's permissions
// instead.
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
String retval = "content://icc/adn/";
try {
long subId = mPhoneAccountRegistrar
@@ -1458,6 +1780,7 @@
return Uri.parse(retval);
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1467,6 +1790,8 @@
*/
@Override
public boolean isTtySupported(String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISTTYSUPPORTED,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.iTS", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "isTtySupported")) {
@@ -1474,10 +1799,12 @@
"READ_PRIVILEGED_PHONE_STATE or READ_PHONE_STATE can call this api");
}
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
return mCallsManager.isTtySupported();
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1487,16 +1814,20 @@
*/
@Override
public int getCurrentTtyMode(String callingPackage, String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETCURRENTTTYMODE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.gCTM", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "getCurrentTtyMode")) {
return TelecomManager.TTY_MODE_OFF;
}
+ event.setResult(ApiStats.RESULT_NORMAL);
synchronized (mLock) {
return mCallsManager.getCurrentTtyMode();
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1507,6 +1838,8 @@
@Override
public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDNEWINCOMINGCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aNIC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
@@ -1540,6 +1873,7 @@
}
}
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
Intent intent = new Intent(TelecomManager.ACTION_INCOMING_CALL);
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
@@ -1551,15 +1885,41 @@
}
mCallIntentProcessorAdapter.processIncomingCallIntent(
mCallsManager, intent);
+ if (mFeatureFlags.earlyBindingToIncallService()) {
+ PhoneAccount account =
+ mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+ phoneAccountHandle);
+ Bundle accountExtra =
+ account == null ? new Bundle() : account.getExtras();
+ PackageManager packageManager = mContext.getPackageManager();
+ // Start binding to InCallServices for wearable calls that do not
+ // require call filtering. This is to wake up default dialer earlier
+ // to mitigate InCallService binding latency.
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && accountExtra != null && accountExtra.getBoolean(
+ PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
+ false)) {
+ 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 {
Binder.restoreCallingIdentity(token);
}
} else {
+ // Invalid parameters are considered as an exception
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.w(this, "Null phoneAccountHandle. Ignoring request to add new" +
" incoming call");
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1570,6 +1930,8 @@
@Override
public void addNewIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDNEWINCOMINGCONFERENCE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aNIC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
@@ -1597,6 +1959,7 @@
}
}
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
mCallsManager.processIncomingConference(
phoneAccountHandle, extras);
@@ -1604,22 +1967,26 @@
Binder.restoreCallingIdentity(token);
}
} else {
+ // Invalid parameters are considered as an exception
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.w(this, "Null phoneAccountHandle. Ignoring request to add new" +
" incoming conference");
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
-
/**
* @see android.telecom.TelecomManager#acceptHandover
*/
@Override
public void acceptHandover(Uri srcAddr, int videoState, PhoneAccountHandle destAcct,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ACCEPTHANDOVER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aHO", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
@@ -1649,17 +2016,22 @@
}
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_EXCEPTION);
try {
mCallsManager.acceptHandover(srcAddr, videoState, destAcct);
+ event.setResult(ApiStats.RESULT_NORMAL);
} finally {
Binder.restoreCallingIdentity(token);
}
} else {
+ // Invalid parameters are considered as an exception
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.w(this, "Null phoneAccountHandle. Ignoring request " +
"to handover the call");
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1669,6 +2041,8 @@
*/
@Override
public void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDNEWUNKNOWNCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.aNUC");
try {
@@ -1692,7 +2066,7 @@
enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
Binder.getCallingUserHandle());
long token = Binder.clearCallingIdentity();
-
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
Intent intent = new Intent(TelecomManager.ACTION_NEW_UNKNOWN_CALL);
if (extras != null) {
@@ -1708,12 +2082,15 @@
Binder.restoreCallingIdentity(token);
}
} else {
+ // Invalid parameters are considered as an exception
+ event.setResult(ApiStats.RESULT_EXCEPTION);
Log.i(this,
"Null phoneAccountHandle or not initiated by Telephony. " +
"Ignoring request to add new unknown call.");
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1724,21 +2101,26 @@
@Override
public void startConference(List<Uri> participants, Bundle extras,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_STARTCONFERENCE,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.sC", Log.getPackageAbbreviation(callingPackage));
if (!canCallPhone(callingPackage, "startConference")) {
throw new SecurityException("Package " + callingPackage + " is not allowed"
+ " to start conference call");
}
-
+ // Binder is clearing the identity, so we need to keep the store the handle
+ UserHandle currentUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
mCallsManager.startConference(participants, extras, callingPackage,
- Binder.getCallingUserHandle());
+ currentUserHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1749,6 +2131,8 @@
@Override
public void placeCall(Uri handle, Bundle extras, String callingPackage,
String callingFeatureId) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_PLACECALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.pC", Log.getPackageAbbreviation(callingPackage));
enforceCallingPackage(callingPackage, "placeCall");
@@ -1824,6 +2208,7 @@
synchronized (mLock) {
final UserHandle userHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
final Intent intent = new Intent(hasCallPrivilegedPermission ?
Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, handle);
@@ -1841,6 +2226,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1850,11 +2236,14 @@
*/
@Override
public boolean enablePhoneAccount(PhoneAccountHandle accountHandle, boolean isEnabled) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ENABLEPHONEACCOUNT,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.ePA");
enforceModifyPermission();
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
// enable/disable phone account
return mPhoneAccountRegistrar.enablePhoneAccount(accountHandle, isEnabled);
@@ -1863,12 +2252,15 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@Override
public boolean setDefaultDialer(String packageName) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_SETDEFAULTDIALER,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.sDD");
enforcePermission(MODIFY_PHONE_STATE);
@@ -1876,6 +2268,7 @@
synchronized (mLock) {
int callerUserId = UserHandle.getCallingUserId();
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return mDefaultDialerCache.setDefaultDialer(packageName,
callerUserId);
@@ -1884,6 +2277,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1900,7 +2294,11 @@
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
try {
- BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+ if (mBlockedNumbersManager != null) {
+ mBlockedNumbersManager.endBlockSuppression();
+ } else {
+ BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1912,11 +2310,15 @@
@Override
public TelecomAnalytics dumpCallAnalytics() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_DUMPCALLANALYTICS,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.dCA");
enforcePermission(DUMP);
+ event.setResult(ApiStats.RESULT_NORMAL);
return Analytics.dumpToParcelableAnalytics();
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -1931,6 +2333,8 @@
*/
@Override
protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_DUMP,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
if (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
@@ -1940,11 +2344,17 @@
return;
}
+ event.setResult(ApiStats.RESULT_NORMAL);
+ logEvent(event);
if (args != null && args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(
args[0])) {
- Binder.withCleanCallingIdentity(() ->
- Analytics.dumpToEncodedProto(mContext, writer, args));
+ long token = Binder.clearCallingIdentity();
+ try {
+ Analytics.dumpToEncodedProto(mContext, writer, args);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
return;
}
@@ -1967,6 +2377,16 @@
pw.increaseIndent();
Analytics.dump(pw);
pw.decreaseIndent();
+
+ pw.println("Flag Configurations: ");
+ pw.increaseIndent();
+ reflectAndPrintFlagConfigs(pw);
+ pw.decreaseIndent();
+
+ pw.println("TransactionManager: ");
+ pw.increaseIndent();
+ TransactionManager.getInstance().dump(pw);
+ pw.decreaseIndent();
}
if (isTimeLineView) {
Log.dumpEventsTimeline(pw);
@@ -1975,22 +2395,70 @@
}
}
+ @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.
+ */
+ private void reflectAndPrintFlagConfigs(IndentingPrintWriter pw) {
+
+ try {
+ // Look away, a forbidden technique (reflection) is being used to allow us to get
+ // all flag configs without having to add them manually to this method.
+ Method[] methods = FeatureFlags.class.getMethods();
+ 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) {
+ 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]");
+ }
+
+ }
+
/**
* @see android.telecom.TelecomManager#createManageBlockedNumbersIntent
*/
@Override
public Intent createManageBlockedNumbersIntent(String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_CREATEMANAGEBLOCKEDNUMBERSINTENT,
+ Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
try {
Log.startSession("TSI.cMBNI", Log.getPackageAbbreviation(callingPackage));
return BlockedNumbersActivity.getIntentForStartingActivity();
} finally {
+ logEvent(event);
Log.endSession();
}
}
-
@Override
public Intent createLaunchEmergencyDialerIntent(String number) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+ ApiStats.API_CREATELAUNCHEMERGENCYDIALERINTENT,
+ Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
String packageName = mContext.getApplicationContext().getString(
com.android.internal.R.string.config_emergency_dialer_package);
Intent intent = new Intent(Intent.ACTION_DIAL_EMERGENCY)
@@ -2003,6 +2471,7 @@
if (!TextUtils.isEmpty(number) && TextUtils.isDigitsOnly(number)) {
intent.setData(Uri.parse("tel:" + number));
}
+ logEvent(event);
return intent;
}
@@ -2012,6 +2481,8 @@
@Override
public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINCOMINGCALLPERMITTED,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
Log.startSession("TSI.iICP", Log.getPackageAbbreviation(callingPackage));
try {
enforceCallingPackage(callingPackage, "isIncomingCallPermitted");
@@ -2020,6 +2491,7 @@
enforceUserHandleMatchesCaller(phoneAccountHandle);
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return mCallsManager.isIncomingCallPermitted(phoneAccountHandle);
} finally {
@@ -2027,6 +2499,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -2037,6 +2510,8 @@
@Override
public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISOUTGOINGCALLPERMITTED,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
Log.startSession("TSI.iOCP", Log.getPackageAbbreviation(callingPackage));
try {
enforceCallingPackage(callingPackage, "isOutgoingCallPermitted");
@@ -2045,6 +2520,7 @@
enforceUserHandleMatchesCaller(phoneAccountHandle);
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
return mCallsManager.isOutgoingCallPermitted(phoneAccountHandle);
} finally {
@@ -2052,6 +2528,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -2105,11 +2582,14 @@
*/
@Override
public boolean isInEmergencyCall() {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINEMERGENCYCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
Log.startSession("TSI.iIEC");
enforceModifyPermission();
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
boolean isInEmergencyCall = mCallsManager.isInEmergencyCall();
Log.i(this, "isInEmergencyCall: %b", isInEmergencyCall);
@@ -2119,6 +2599,7 @@
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
@@ -2138,7 +2619,7 @@
try {
Log.i(this, "handleCallIntent: handling call intent");
mCallIntentProcessorAdapter.processOutgoingCallIntent(mContext,
- mCallsManager, intent, callingPackage);
+ mCallsManager, intent, callingPackage, mFeatureFlags);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2149,14 +2630,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
@@ -2165,26 +2642,42 @@
try {
synchronized (mLock) {
enforceShellOnly(Binder.getCallingUid(), "cleanupStuckCalls");
- Binder.withCleanCallingIdentity(() -> {
+ long token = Binder.clearCallingIdentity();
+ 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) {
mCallsManager.getInCallController().unbindFromServices(userHandle);
}
- });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
} finally {
Log.endSession();
}
}
+ 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}
@@ -2212,6 +2705,39 @@
}
/**
+ * A method intended for use in testing to query whether a particular non-ui inCallService
+ * is bound in a call.
+ * @param packageName of the service to query.
+ * @return whether it is bound or not.
+ */
+ @Override
+ public boolean isNonUiInCallServiceBound(String packageName) {
+ Log.startSession("TCI.iNUICSB");
+ try {
+ synchronized (mLock) {
+ enforceShellOnly(Binder.getCallingUid(), "isNonUiInCallServiceBound");
+ if (!(mContext.checkCallingOrSelfPermission(READ_PHONE_STATE)
+ == PackageManager.PERMISSION_GRANTED) ||
+ !(mContext.checkCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE)
+ == PackageManager.PERMISSION_GRANTED)) {
+ throw new SecurityException("isNonUiInCallServiceBound requires the"
+ + " READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE permission");
+ }
+ long token = Binder.clearCallingIdentity();
+ try {
+ return mCallsManager
+ .getInCallController()
+ .isNonUiInCallServiceBound(packageName);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ /**
* A method intended for use in testing to reset car mode at all priorities.
*
* Runs during setup to avoid cascading failures from failing car mode CTS.
@@ -2222,11 +2748,14 @@
try {
synchronized (mLock) {
enforceShellOnly(Binder.getCallingUid(), "resetCarMode");
- Binder.withCleanCallingIdentity(() -> {
+ long token = Binder.clearCallingIdentity();
+ try {
UiModeManager uiModeManager =
mContext.getSystemService(UiModeManager.class);
uiModeManager.disableCarMode(UiModeManager.DISABLE_CAR_MODE_ALL_PRIORITIES);
- });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
} finally {
Log.endSession();
@@ -2316,7 +2845,8 @@
}
@Override
- public void setTestPhoneAcctSuggestionComponent(String flattenedComponentName) {
+ public void setTestPhoneAcctSuggestionComponent(String flattenedComponentName,
+ UserHandle userHandle) {
try {
Log.startSession("TSI.sPASA");
enforceModifyPermission();
@@ -2326,6 +2856,7 @@
}
synchronized (mLock) {
PhoneAccountSuggestionHelper.setOverrideServiceName(flattenedComponentName);
+ PhoneAccountSuggestionHelper.setOverrideUserHandle(userHandle);
}
} finally {
Log.endSession();
@@ -2390,27 +2921,93 @@
@Override
public boolean isInSelfManagedCall(String packageName, UserHandle userHandle,
String callingPackage) {
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINSELFMANAGEDCALL,
+ Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
try {
- if (Binder.getCallingUid() != Process.SYSTEM_UID) {
- throw new SecurityException("Only the system can call this API");
- }
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 (!Binder.getCallingUserHandle().equals(userHandle)) {
+ enforceInAppCrossUserPermission();
+ }
Log.startSession("TSI.iISMC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
+ event.setResult(ApiStats.RESULT_NORMAL);
try {
- return mCallsManager.isInSelfManagedCall(packageName, userHandle);
+ return mCallsManager.isInSelfManagedCall(
+ packageName, userHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
}
} finally {
+ logEvent(event);
Log.endSession();
}
}
};
+ public TelecomServiceImpl(
+ Context context,
+ CallsManager callsManager,
+ PhoneAccountRegistrar phoneAccountRegistrar,
+ CallIntentProcessor.Adapter callIntentProcessorAdapter,
+ UserCallIntentProcessorFactory userCallIntentProcessorFactory,
+ DefaultDialerCache defaultDialerCache,
+ SubscriptionManagerAdapter subscriptionManagerAdapter,
+ SettingsSecureAdapter settingsSecureAdapter,
+ FeatureFlags featureFlags,
+ com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags,
+ TelecomSystem.SyncRoot lock, TelecomMetricsController metricsController,
+ String sysUiPackageName) {
+ mContext = context;
+ mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+
+ mPackageManager = mContext.getPackageManager();
+
+ mCallsManager = callsManager;
+ mFeatureFlags = featureFlags;
+ if (telephonyFeatureFlags != null) {
+ mTelephonyFeatureFlags = telephonyFeatureFlags;
+ } else {
+ mTelephonyFeatureFlags =
+ new com.android.internal.telephony.flags.FeatureFlagsImpl();
+ }
+ mLock = lock;
+ mPhoneAccountRegistrar = phoneAccountRegistrar;
+ mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
+ mDefaultDialerCache = defaultDialerCache;
+ mCallIntentProcessorAdapter = callIntentProcessorAdapter;
+ mSubscriptionManagerAdapter = subscriptionManagerAdapter;
+ mSettingsSecureAdapter = settingsSecureAdapter;
+ mMetricsController = metricsController;
+ mSystemUiPackageName = sysUiPackageName;
+
+ mDefaultDialerCache.observeDefaultDialerApplication(mContext.getMainExecutor(), userId -> {
+ String defaultDialer = mDefaultDialerCache.getDefaultDialerApplication(userId);
+ if (defaultDialer == null) {
+ // We are replacing the dialer, just wait for the upcoming callback.
+ return;
+ }
+ final Intent intent = new Intent(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED)
+ .putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
+ defaultDialer);
+ mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
+ });
+
+ mTransactionManager = TransactionManager.getInstance();
+ mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+ mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+ ? mContext.getSystemService(BlockedNumbersManager.class)
+ : null;
+ }
+
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter) {
+ mAnomalyReporter = mAnomalyReporterAdapter;
+ }
private boolean enforceCallStreamingPermission(String packageName, PhoneAccountHandle handle,
int uid) {
@@ -2440,7 +3037,7 @@
final int opCode = AppOpsManager.permissionToOpCode(permission);
if (opCode != AppOpsManager.OP_NONE
&& mAppOpsManager.checkOp(opCode, uid, packageName)
- != AppOpsManager.MODE_ALLOWED) {
+ != AppOpsManager.MODE_ALLOWED) {
return false;
}
}
@@ -2457,70 +3054,12 @@
"App requires ACCEPT_HANDOVER permission to accept handovers.");
final int opCode = AppOpsManager.permissionToOpCode(Manifest.permission.ACCEPT_HANDOVER);
- if (opCode != AppOpsManager.OP_ACCEPT_HANDOVER || (
- mAppOpsManager.checkOp(opCode, uid, packageName)
- != AppOpsManager.MODE_ALLOWED)) {
- return false;
- }
- return true;
- }
-
- private Context mContext;
- private AppOpsManager mAppOpsManager;
- private PackageManager mPackageManager;
- private CallsManager mCallsManager;
- private final PhoneAccountRegistrar mPhoneAccountRegistrar;
- private final CallIntentProcessor.Adapter mCallIntentProcessorAdapter;
- private final UserCallIntentProcessorFactory mUserCallIntentProcessorFactory;
- private final DefaultDialerCache mDefaultDialerCache;
- private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
- private final SettingsSecureAdapter mSettingsSecureAdapter;
- private final TelecomSystem.SyncRoot mLock;
- private TransactionManager mTransactionManager;
- private final TransactionalServiceRepository mTransactionalServiceRepository;
-
- public TelecomServiceImpl(
- Context context,
- CallsManager callsManager,
- PhoneAccountRegistrar phoneAccountRegistrar,
- CallIntentProcessor.Adapter callIntentProcessorAdapter,
- UserCallIntentProcessorFactory userCallIntentProcessorFactory,
- DefaultDialerCache defaultDialerCache,
- SubscriptionManagerAdapter subscriptionManagerAdapter,
- SettingsSecureAdapter settingsSecureAdapter,
- TelecomSystem.SyncRoot lock) {
- mContext = context;
- mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
-
- mPackageManager = mContext.getPackageManager();
-
- mCallsManager = callsManager;
- mLock = lock;
- mPhoneAccountRegistrar = phoneAccountRegistrar;
- mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
- mDefaultDialerCache = defaultDialerCache;
- mCallIntentProcessorAdapter = callIntentProcessorAdapter;
- mSubscriptionManagerAdapter = subscriptionManagerAdapter;
- mSettingsSecureAdapter = settingsSecureAdapter;
-
- mDefaultDialerCache.observeDefaultDialerApplication(mContext.getMainExecutor(), userId -> {
- String defaultDialer = mDefaultDialerCache.getDefaultDialerApplication(userId);
- if (defaultDialer == null) {
- // We are replacing the dialer, just wait for the upcoming callback.
- return;
- }
- final Intent intent = new Intent(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED)
- .putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
- defaultDialer);
- mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
- });
-
- mTransactionManager = TransactionManager.getInstance();
- mTransactionalServiceRepository = new TransactionalServiceRepository();
+ return opCode == AppOpsManager.OP_ACCEPT_HANDOVER
+ && (mAppOpsManager.checkOp(opCode, uid, packageName) == AppOpsManager.MODE_ALLOWED);
}
@VisibleForTesting
- public void setTransactionManager(TransactionManager transactionManager){
+ public void setTransactionManager(TransactionManager transactionManager) {
mTransactionManager = transactionManager;
}
@@ -2528,10 +3067,6 @@
return mBinderImpl;
}
- //
- // Supporting methods for the ITelecomService interface implementation.
- //
-
private boolean isPhoneAccountHandleVisibleToCallingUser(
PhoneAccountHandle phoneAccountUserHandle, UserHandle callingUser) {
synchronized (mLock) {
@@ -2563,13 +3098,14 @@
return false;
}
- private void acceptRingingCallInternal(int videoState, String packageName) {
+ private void acceptRingingCallInternal(int videoState, String packageName,
+ boolean isCallerPrivileged) {
Call call = mCallsManager.getFirstCallWithState(CallState.RINGING,
CallState.SIMULATED_RINGING);
if (call != null) {
- if (call.isSelfManaged()) {
+ if (call.isSelfManaged() && !isCallerPrivileged) {
Log.addEvent(call, LogUtils.Events.REQUEST_ACCEPT,
- "self-mgd accept ignored from " + packageName);
+ "self-mgd accept ignored from non-privileged app " + packageName);
return;
}
@@ -2580,7 +3116,11 @@
}
}
- private boolean endCallInternal(String callingPackage) {
+ //
+ // Supporting methods for the ITelecomService interface implementation.
+ //
+
+ private boolean endCallInternal(String callingPackage, boolean isCallerPrivileged) {
// Always operate on the foreground call if one exists, otherwise get the first call in
// priority order by call-state.
Call call = mCallsManager.getForegroundCall();
@@ -2600,9 +3140,10 @@
return false;
}
- if (call.isSelfManaged()) {
+ if (call.isSelfManaged() && !isCallerPrivileged) {
Log.addEvent(call, LogUtils.Events.REQUEST_DISCONNECT,
- "self-mgd disconnect ignored from " + callingPackage);
+ "self-mgd disconnect ignored from non-privileged app " +
+ callingPackage);
return false;
}
@@ -2621,14 +3162,14 @@
// Enforce that the PhoneAccountHandle being passed in is both registered to the current user
// and enabled.
private void enforcePhoneAccountIsRegisteredEnabled(PhoneAccountHandle phoneAccountHandle,
- UserHandle callingUserHandle) {
+ UserHandle callingUserHandle) {
PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle,
callingUserHandle);
- if(phoneAccount == null) {
+ if (phoneAccount == null) {
EventLog.writeEvent(0x534e4554, "26864502", Binder.getCallingUid(), "R");
throw new SecurityException("This PhoneAccountHandle is not registered for this user!");
}
- if(!phoneAccount.isEnabled()) {
+ if (!phoneAccount.isEnabled()) {
EventLog.writeEvent(0x534e4554, "26864502", Binder.getCallingUid(), "E");
throw new SecurityException("This PhoneAccountHandle is not enabled for this user!");
}
@@ -2700,7 +3241,7 @@
/**
* helper method that compares the binder_uid to what the packageManager_uid reports for the
* passed in packageName.
- *
+ * <p>
* returns true if the binder_uid matches the packageManager_uid records
*/
private boolean callingUidMatchesPackageManagerRecords(String packageName) {
@@ -2708,13 +3249,12 @@
int callingUid = Binder.getCallingUid();
PackageManager pm;
long token = Binder.clearCallingIdentity();
- try{
+ try {
pm = mContext.createContextAsUser(
UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
- }
- catch (Exception e){
+ } catch (Exception e) {
Log.i(this, "callingUidMatchesPackageManagerRecords:"
- + " createContextAsUser hit exception=[%s]", e.toString());
+ + " createContextAsUser hit exception=[%s]", e.toString());
return false;
} finally {
Binder.restoreCallingIdentity(token);
@@ -2729,7 +3269,7 @@
if (packageUid != callingUid) {
Log.i(this, "callingUidMatchesPackageManagerRecords: uid mismatch found for"
- + "packageName=[%s]. packageManager reports packageUid=[%d] but "
+ + "packageName=[%s]. packageManager reports packageUid=[%d] but "
+ "binder reports callingUid=[%d]", packageName, packageUid, callingUid);
}
@@ -2820,7 +3360,7 @@
boolean permissionsOk =
isCallerSimCallManagerForAnySim(account.getAccountHandle())
|| mContext.checkCallingOrSelfPermission(REGISTER_SIM_SUBSCRIPTION)
- == PackageManager.PERMISSION_GRANTED;
+ == PackageManager.PERMISSION_GRANTED;
if (!prerequisiteCapabilitiesOk || !permissionsOk) {
throw new SecurityException(
"Only SIM subscriptions and connection managers are allowed to declare "
@@ -2832,7 +3372,7 @@
private void enforceRegisterSkipCallFiltering() {
if (!isCallerSystemApp()) {
throw new SecurityException(
- "EXTRA_SKIP_CALL_FILTERING is only available to system apps.");
+ "EXTRA_SKIP_CALL_FILTERING is only available to system apps.");
}
}
@@ -2865,12 +3405,24 @@
+ " INTERACT_ACROSS_USERS permission");
}
+ private void enforceInAppCrossProfilePermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.INTERACT_ACROSS_PROFILES, "Must be system or have"
+ + " INTERACT_ACROSS_PROFILES permission");
+ }
+
private boolean hasInAppCrossUserPermission() {
return mContext.checkCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS)
== PackageManager.PERMISSION_GRANTED;
}
+ private boolean hasInAppCrossProfilePermission() {
+ return mContext.checkCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_PROFILES)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
// to be used for TestApi methods that can only be called with SHELL UID.
private void enforceShellOnly(int callingUid, String message) {
if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) {
@@ -2964,7 +3516,6 @@
+ " does not meet the requirements to access the phone number");
}
-
private boolean canReadPrivilegedPhoneState(String callingPackage, String message) {
// The system/default dialer can always read phone state - so that emergency calls will
// still work.
@@ -2991,9 +3542,9 @@
private boolean isSelfManagedConnectionService(PhoneAccountHandle phoneAccountHandle) {
if (phoneAccountHandle != null) {
- PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
- phoneAccountHandle);
- return phoneAccount != null && phoneAccount.isSelfManaged();
+ PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+ phoneAccountHandle);
+ return phoneAccount != null && phoneAccount.isSelfManaged();
}
return false;
}
@@ -3095,10 +3646,11 @@
// Note: Important to clear the calling identity since the code below calls into RoleManager
// to check who holds the dialer role, and that requires MANAGE_ROLE_HOLDERS permission
// which is a system permission.
+ int callingUserId = Binder.getCallingUserHandle().getIdentifier();
long token = Binder.clearCallingIdentity();
try {
return mDefaultDialerCache.isDefaultOrSystemDialer(
- callingPackage, Binder.getCallingUserHandle().getIdentifier());
+ callingPackage, callingUserId);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -3129,7 +3681,7 @@
}
private void broadcastCallScreeningAppChangedIntent(String componentName,
- boolean isDefault) {
+ boolean isDefault) {
if (TextUtils.isEmpty(componentName)) {
return;
}
@@ -3138,11 +3690,11 @@
if (broadcastComponentName != null) {
Intent intent = new Intent(TelecomManager
- .ACTION_DEFAULT_CALL_SCREENING_APP_CHANGED);
+ .ACTION_DEFAULT_CALL_SCREENING_APP_CHANGED);
intent.putExtra(TelecomManager
- .EXTRA_IS_DEFAULT_CALL_SCREENING_APP, isDefault);
+ .EXTRA_IS_DEFAULT_CALL_SCREENING_APP, isDefault);
intent.putExtra(TelecomManager
- .EXTRA_DEFAULT_CALL_SCREENING_APP_COMPONENT_NAME, componentName);
+ .EXTRA_DEFAULT_CALL_SCREENING_APP_COMPONENT_NAME, componentName);
intent.setPackage(broadcastComponentName.getPackageName());
mContext.sendBroadcast(intent);
}
@@ -3165,4 +3717,56 @@
}
}
}
+
+ private void validateSimultaneousCallingPackageNames(String appPackageName,
+ Set<PhoneAccountHandle> handles) {
+ for (PhoneAccountHandle handle : handles) {
+ ComponentName name = handle.getComponentName();
+ if (name == null) {
+ throw new IllegalArgumentException("ComponentName is null");
+ }
+ String restrictionPackageName = name.getPackageName();
+ if (!appPackageName.equals(restrictionPackageName)) {
+ throw new SecurityException("The package name of the PhoneAccount does not "
+ + "match one or more of the package names set in the simultaneous "
+ + "calling restriction.");
+ }
+ }
+ }
+
+ private void logEvent(ApiStats.ApiEvent event) {
+ if (mFeatureFlags.telecomMetricsSupport()) {
+ mMetricsController.getApiStats().log(event);
+ }
+ }
+
+ public interface SubscriptionManagerAdapter {
+ int getDefaultVoiceSubId();
+ }
+
+ public interface SettingsSecureAdapter {
+ void putStringForUser(ContentResolver resolver, String name, String value, int userHandle);
+
+ String getStringForUser(ContentResolver resolver, String name, int userHandle);
+ }
+
+ static class SubscriptionManagerAdapterImpl implements SubscriptionManagerAdapter {
+ @Override
+ public int getDefaultVoiceSubId() {
+ return SubscriptionManager.getDefaultVoiceSubscriptionId();
+ }
+ }
+
+ static class SettingsSecureAdapterImpl implements SettingsSecureAdapter {
+ @Override
+ public void putStringForUser(ContentResolver resolver, String name, String value,
+ int userHandle) {
+ Settings.Secure.putStringForUser(resolver, name, value, userHandle);
+ }
+
+ @Override
+ public String getStringForUser(ContentResolver resolver, String name, int userHandle) {
+ return Settings.Secure.getStringForUser(resolver, name, userHandle);
+ }
+ }
}
diff --git a/src/com/android/server/telecom/TelecomShellCommand.java b/src/com/android/server/telecom/TelecomShellCommand.java
new file mode 100644
index 0000000..11ceb26
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomShellCommand.java
@@ -0,0 +1,530 @@
+/*
+ * 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();
+ final UserHandle userHandle = getUserHandleFromArgs();
+ mTelecomService.setTestPhoneAcctSuggestionComponent(componentName, userHandle);
+ }
+
+ 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 UserHandle getUserHandleFromArgs() throws RemoteException {
+ if (TextUtils.isEmpty(peekNextArg())) {
+ return null;
+ }
+ 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 userHandle;
+ }
+
+ 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 da325f7..7020885 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,15 +44,18 @@
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.IncomingCallFilterGraph;
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.TransactionManager;
import java.io.FileNotFoundException;
import java.io.InputStream;
@@ -137,6 +139,7 @@
private final TelecomServiceImpl mTelecomServiceImpl;
private final ContactsAsyncHelper mContactsAsyncHelper;
private final DialerCodeReceiver mDialerCodeReceiver;
+ private final FeatureFlags mFeatureFlags;
private boolean mIsBootComplete = false;
@@ -221,11 +224,15 @@
RoleManagerAdapter roleManagerAdapter,
ContactsAsyncHelper.Factory contactsAsyncHelperFactory,
DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ String sysUiPackageName,
Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
Executor asyncTaskExecutor,
Executor asyncCallAudioTaskExecutor,
- BlockedNumbersAdapter blockedNumbersAdapter) {
+ BlockedNumbersAdapter blockedNumbersAdapter,
+ FeatureFlags featureFlags,
+ com.android.internal.telephony.flags.FeatureFlags telephonyFlags) {
mContext = context.getApplicationContext();
+ mFeatureFlags = featureFlags;
LogUtils.initLogging(mContext);
android.telecom.Log.setLock(mLock);
AnomalyReporter.initialize(mContext);
@@ -239,8 +246,8 @@
// Wrap this in a try block to ensure session cleanup occurs in the case of error.
try {
mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
- packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName));
+ (packageName, userHandle) -> AppLabelProxy.Util.getAppLabel(mContext,
+ userHandle, packageName, mFeatureFlags), null, mFeatureFlags);
mContactsAsyncHelper = contactsAsyncHelperFactory.create(
new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -250,13 +257,19 @@
return context.getContentResolver().openInputStream(uri);
}
});
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker = new
+ CallAudioCommunicationDeviceTracker(mContext);
BluetoothDeviceManager bluetoothDeviceManager = new BluetoothDeviceManager(mContext,
- mContext.getSystemService(BluetoothManager.class).getAdapter());
+ mContext.getSystemService(BluetoothManager.class).getAdapter(),
+ communicationDeviceTracker, featureFlags);
BluetoothRouteManager bluetoothRouteManager = new BluetoothRouteManager(mContext, mLock,
- bluetoothDeviceManager, new Timeouts.Adapter());
+ bluetoothDeviceManager, new Timeouts.Adapter(),
+ communicationDeviceTracker, featureFlags);
BluetoothStateReceiver bluetoothStateReceiver = new BluetoothStateReceiver(
- bluetoothDeviceManager, bluetoothRouteManager);
+ bluetoothDeviceManager, bluetoothRouteManager,
+ communicationDeviceTracker, featureFlags);
mContext.registerReceiver(bluetoothStateReceiver, BluetoothStateReceiver.INTENT_FILTER);
+ communicationDeviceTracker.setBluetoothRouteManager(bluetoothRouteManager);
WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
SystemStateHelper systemStateHelper = new SystemStateHelper(mContext, mLock);
@@ -264,7 +277,8 @@
mMissedCallNotifier = missedCallNotifierImplFactory
.makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar,
defaultDialerCache,
- deviceIdleControllerAdapter);
+ deviceIdleControllerAdapter,
+ featureFlags);
DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory =
new DisconnectedCallNotifier.Default();
@@ -273,7 +287,7 @@
mContactsAsyncHelper, mLock);
EmergencyCallHelper emergencyCallHelper = new EmergencyCallHelper(mContext,
- defaultDialerCache, timeoutsAdapter);
+ defaultDialerCache, timeoutsAdapter, mFeatureFlags);
InCallControllerFactory inCallControllerFactory = new InCallControllerFactory() {
@Override
@@ -283,7 +297,7 @@
EmergencyCallHelper emergencyCallHelper) {
return new InCallController(context, lock, callsManager, systemStateProvider,
defaultDialerCache, timeoutsAdapter, emergencyCallHelper,
- new CarModeTracker(), clockProxy);
+ new CarModeTracker(), clockProxy, featureFlags);
}
};
@@ -292,7 +306,7 @@
@Override
public CallEndpointController create(Context context, SyncRoot lock,
CallsManager callsManager) {
- return new CallEndpointController(context, callsManager);
+ return new CallEndpointController(context, callsManager, featureFlags);
}
};
@@ -334,15 +348,24 @@
ToastFactory toastFactory = new ToastFactory() {
@Override
- public Toast makeText(Context context, int resId, int duration) {
- return Toast.makeText(context, context.getMainLooper(),
- context.getString(resId),
- duration);
+ public void makeText(Context context, int resId, int duration) {
+ if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+ context.getMainExecutor().execute(() ->
+ Toast.makeText(context, resId, duration).show());
+ } else {
+ Toast.makeText(context, context.getMainLooper(),
+ context.getString(resId), duration).show();
+ }
}
@Override
- public Toast makeText(Context context, CharSequence text, int duration) {
- return Toast.makeText(context, context.getMainLooper(), text, duration);
+ public void makeText(Context context, CharSequence text, int duration) {
+ if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+ context.getMainExecutor().execute(() ->
+ Toast.makeText(context, text, duration).show());
+ } else {
+ Toast.makeText(context, context.getMainLooper(), text, duration).show();
+ }
}
};
@@ -352,16 +375,20 @@
BugreportManager.class), timeoutsAdapter, mContext.getSystemService(
DropBoxManager.class), asyncTaskExecutor, clockProxy);
+ TelecomMetricsController metricsController = featureFlags.telecomMetricsSupport()
+ ? TelecomMetricsController.make(mContext) : null;
+
CallAnomalyWatchdog callAnomalyWatchdog = new CallAnomalyWatchdog(
Executors.newSingleThreadScheduledExecutor(),
- mLock, timeoutsAdapter, clockProxy, emergencyCallDiagnosticLogger);
+ mLock, mFeatureFlags, timeoutsAdapter, clockProxy,
+ emergencyCallDiagnosticLogger, metricsController);
TransactionManager transactionManager = TransactionManager.getInstance();
CallStreamingNotification callStreamingNotification =
new CallStreamingNotification(mContext,
- packageName -> AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), packageName), asyncTaskExecutor);
+ (packageName, userHandle) -> AppLabelProxy.Util.getAppLabel(mContext,
+ userHandle, packageName, mFeatureFlags), asyncTaskExecutor);
mCallsManager = new CallsManager(
mContext,
@@ -401,7 +428,13 @@
blockedNumbersAdapter,
transactionManager,
emergencyCallDiagnosticLogger,
- callStreamingNotification);
+ communicationDeviceTracker,
+ callStreamingNotification,
+ bluetoothDeviceManager,
+ featureFlags,
+ telephonyFlags,
+ IncomingCallFilterGraph::new,
+ metricsController);
mIncomingCallNotifier = incomingCallNotifier;
incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
@@ -445,7 +478,7 @@
}
mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager,
- defaultDialerCache);
+ defaultDialerCache, featureFlags);
mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
mContext, mCallsManager);
@@ -455,7 +488,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),
@@ -463,13 +495,17 @@
@Override
public UserCallIntentProcessor create(Context context,
UserHandle userHandle) {
- return new UserCallIntentProcessor(context, userHandle);
+ return new UserCallIntentProcessor(context, userHandle, featureFlags);
}
},
defaultDialerCache,
new TelecomServiceImpl.SubscriptionManagerAdapterImpl(),
new TelecomServiceImpl.SettingsSecureAdapterImpl(),
- mLock);
+ featureFlags,
+ null,
+ mLock,
+ metricsController,
+ sysUiPackageName);
} finally {
Log.endSession();
}
@@ -504,4 +540,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 c5fdd4c..ee18250 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -41,6 +41,10 @@
return Timeouts.getCallScreeningTimeoutMillis(cr);
}
+ public long getCallBindBluetoothInCallServicesDelay(ContentResolver cr) {
+ return Timeouts.getCallBindBluetoothInCallServicesDelay(cr);
+ }
+
public long getCallRemoveUnbindInCallServicesDelay(ContentResolver cr) {
return Timeouts.getCallRemoveUnbindInCallServicesDelay(cr);
}
@@ -57,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);
}
@@ -123,7 +135,6 @@
public int getDaysBackToSearchEmergencyDiagnosticEntries(){
return Timeouts.getDaysBackToSearchEmergencyDiagnosticEntries();
-
}
}
@@ -270,6 +281,11 @@
60000L /* 1 minute */);
}
+ public static long getCallBindBluetoothInCallServicesDelay(ContentResolver contentResolver) {
+ return get(contentResolver, "call_bind_bluetooth_in_call_services_delay",
+ 2000L /* 2 seconds */);
+ }
+
/**
* Returns the amount of delay before unbinding the in-call services after all the calls
* are removed.
@@ -431,12 +447,12 @@
/**
* Returns the duration of time a VoIP call can be in an intermediate state before Telecom will
- * try to clean up the call.
+ * try to clean up the call. The default is 2 minutes.
* @return the state timeout in millis.
*/
public static long getVoipCallIntermediateStateTimeoutMillis() {
return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
- INTERMEDIATE_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 60000L);
+ INTERMEDIATE_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 120000L);
}
/**
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 15278e1..5ae459e 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -20,6 +20,8 @@
import android.telecom.PhoneAccountHandle;
import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.callsequencing.TransactionManager;
import java.util.HashMap;
import java.util.Map;
@@ -32,8 +34,10 @@
private static final String TAG = TransactionalServiceRepository.class.getSimpleName();
private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
new HashMap<>();
+ private final FeatureFlags mFlags;
- public TransactionalServiceRepository() {
+ public TransactionalServiceRepository(FeatureFlags flags) {
+ mFlags = flags;
}
public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -45,7 +49,8 @@
if (!hasExistingServiceWrapper(phoneAccountHandle)) {
Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
service = new TransactionalServiceWrapper(callEventCallback,
- callsManager, phoneAccountHandle, call, this);
+ callsManager, phoneAccountHandle, call, this,
+ TransactionManager.getInstance(), mFlags.enableCallSequencing());
} else {
Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
service = getTransactionalServiceWrapper(phoneAccountHandle);
@@ -61,11 +66,11 @@
return service;
}
- public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+ private TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
return mServiceLookupTable.get(pah);
}
- public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
return mServiceLookupTable.containsKey(pah);
}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 25aaad7..cf5ef41 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;
@@ -38,22 +39,18 @@
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
-import com.android.server.telecom.voip.CallEventCallbackAckTransaction;
-import com.android.server.telecom.voip.EndpointChangeTransaction;
-import com.android.server.telecom.voip.HoldCallTransaction;
-import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.ParallelTransaction;
-import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionalCallSequencingAdapter;
+import com.android.server.telecom.callsequencing.voip.CallEventCallbackAckTransaction;
+import com.android.server.telecom.callsequencing.voip.EndpointChangeTransaction;
+import com.android.server.telecom.callsequencing.voip.SetMuteStateTransaction;
+import com.android.server.telecom.callsequencing.voip.RequestVideoStateTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -61,7 +58,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
@@ -70,6 +67,9 @@
public static final String ANSWER = "Answer";
public static final String DISCONNECT = "Disconnect";
public static final String START_STREAMING = "StartStreaming";
+ public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
+ public static final String SET_MUTE_STATE = "SetMuteState";
+ public static final String CALL_ENDPOINT_CHANGE = "CallEndpointChange";
// CallEventCallback : Telecom --> Client (ex. voip app)
public static final String ON_SET_ACTIVE = "onSetActive";
@@ -77,6 +77,7 @@
public static final String ON_ANSWER = "onAnswer";
public static final String ON_DISCONNECT = "onDisconnect";
public static final String ON_STREAMING_STARTED = "onStreamingStarted";
+ public static final String STOP_STREAMING = "stopStreaming";
private final CallsManager mCallsManager;
private final ICallEventCallback mICallEventCallback;
@@ -90,6 +91,7 @@
// needs to be non-final for testing
private TransactionManager mTransactionManager;
private CallStreamingController mStreamingController;
+ private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
// Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
@@ -105,30 +107,29 @@
public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
- TransactionalServiceRepository repo) {
+ TransactionalServiceRepository repo, TransactionManager transactionManager,
+ boolean isCallSequencingEnabled) {
// passed args
mICallEventCallback = callEventCallback;
mCallsManager = callsManager;
mPhoneAccountHandle = phoneAccountHandle;
mTrackedCalls.put(call.getId(), call); // service is now tracking its first call
mRepository = repo;
+ mTransactionManager = transactionManager;
// init instance vars
mPackageName = phoneAccountHandle.getComponentName().getPackageName();
- mTransactionManager = TransactionManager.getInstance();
mStreamingController = mCallsManager.getCallStreamingController();
mLock = mCallsManager.getLock();
+ mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
+ mCallsManager, isCallSequencingEnabled);
setDeathRecipient(callEventCallback);
}
- @VisibleForTesting
- public void setTransactionManager(TransactionManager transactionManager) {
- mTransactionManager = transactionManager;
- }
-
public TransactionManager getTransactionManager() {
return mTransactionManager;
}
+ @VisibleForTesting
public PhoneAccountHandle getPhoneAccountHandle() {
return mPhoneAccountHandle;
}
@@ -165,12 +166,8 @@
return callCount;
}
- public void cleanupTransactionalServiceWrapper() {
- for (Call call : mTrackedCalls.values()) {
- mCallsManager.markCallAsDisconnected(call,
- new DisconnectCause(DisconnectCause.ERROR, "process died"));
- mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
- }
+ private void cleanupTransactionalServiceWrapper() {
+ mCallSequencingAdapter.cleanup(mTrackedCalls.values());
}
/***
@@ -178,59 +175,90 @@
** ICallControl: Client --> Server **
**********************************************************************************************
*/
- public final ICallControl mICallControl = new ICallControl.Stub() {
+ private final ICallControl mICallControl = new ICallControl.Stub() {
@Override
- public void setActive(String callId, android.os.ResultReceiver callback)
- throws RemoteException {
+ public void setActive(String callId, android.os.ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sA");
createTransactions(callId, callback, SET_ACTIVE);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
- public void answer(int videoState, String callId, android.os.ResultReceiver callback)
- throws RemoteException {
+
+ public void answer(int videoState, String callId, android.os.ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.a");
createTransactions(callId, callback, ANSWER, videoState);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
- public void setInactive(String callId, android.os.ResultReceiver callback)
- throws RemoteException {
+ public void setInactive(String callId, android.os.ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sI");
createTransactions(callId, callback, SET_INACTIVE);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void disconnect(String callId, DisconnectCause disconnectCause,
- android.os.ResultReceiver callback)
- throws RemoteException {
+ android.os.ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.d");
createTransactions(callId, callback, DISCONNECT, disconnectCause);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
- public void startCallStreaming(String callId, android.os.ResultReceiver callback)
- throws RemoteException {
+ public void setMuteState(boolean isMuted, android.os.ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ Log.startSession("TSW.sMS");
+ addTransactionsToManager(SET_MUTE_STATE,
+ new SetMuteStateTransaction(mCallsManager, isMuted), callback);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void startCallStreaming(String callId, android.os.ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sCS");
createTransactions(callId, callback, START_STREAMING);
} finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void requestVideoState(int videoState, String callId, ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ Log.startSession("TSW.rVS");
+ createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
+ } finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -242,24 +270,31 @@
if (call != null) {
switch (action) {
case SET_ACTIVE:
- handleCallControlNewCallFocusTransactions(call, SET_ACTIVE,
- false /* isAnswer */, 0/*VideoState (ignored)*/, callback);
+ mCallSequencingAdapter.setActive(call,
+ getCompleteReceiver(action, callback));
break;
case ANSWER:
- handleCallControlNewCallFocusTransactions(call, ANSWER,
- true /* isAnswer */, (int) objects[0] /*VideoState*/, callback);
+ mCallSequencingAdapter.setAnswered(call, (int) objects[0] /*VideoState*/,
+ getCompleteReceiver(action, callback));
break;
case DISCONNECT:
- addTransactionsToManager(new EndCallTransaction(mCallsManager,
- (DisconnectCause) objects[0], call), callback);
+ DisconnectCause dc = (DisconnectCause) objects[0];
+ mCallSequencingAdapter.setDisconnected(call, dc,
+ getCompleteReceiver(action, callback));
break;
case SET_INACTIVE:
- addTransactionsToManager(
- new HoldCallTransaction(mCallsManager, call), callback);
+ mCallSequencingAdapter.setInactive(call,
+ getCompleteReceiver(action,callback));
break;
case START_STREAMING:
- addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
- TransactionalServiceWrapper.this, call, mLock), callback);
+ addTransactionsToManager(action,
+ mStreamingController.getStartStreamingTransaction(mCallsManager,
+ TransactionalServiceWrapper.this, call, mLock), callback);
+ break;
+ case REQUEST_VIDEO_STATE:
+ addTransactionsToManager(action,
+ new RequestVideoStateTransaction(mCallsManager, call,
+ (int) objects[0]), callback);
break;
}
} else {
@@ -275,39 +310,15 @@
}
}
- // The client is request their VoIP call state go ACTIVE/ANSWERED.
- // 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),
- new OutcomeReceiver<>() {
- @Override
- public void onResult(VoipCallTransactionResult result) {
- Log.i(TAG, String.format(Locale.US,
- "%s: onResult: callId=[%s]", action, call.getId()));
- if (isAnswer) {
- call.setVideoState(potentiallyNewVideoState);
- }
- callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
- }
-
- @Override
- public void onError(CallException exception) {
- Bundle extras = new Bundle();
- extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
- callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
- exception.getCode(), extras);
- }
- });
- }
-
@Override
public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+ long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.rCEC");
- addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
- callback);
+ addTransactionsToManager(CALL_ENDPOINT_CHANGE,
+ new EndpointChangeTransaction(endpoint, mCallsManager), callback);
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@@ -317,6 +328,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);
@@ -328,31 +340,37 @@
+ "found. Most likely the call has been disconnected");
}
} finally {
+ Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
};
- public void addTransactionsToManager(VoipCallTransaction transaction,
+ private void addTransactionsToManager(String action, CallTransaction transaction,
ResultReceiver callback) {
Log.d(TAG, "addTransactionsToManager");
+ CompletableFuture<Boolean> transactionResult = mTransactionManager
+ .addTransaction(transaction, getCompleteReceiver(action, callback));
+ }
- mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+ private OutcomeReceiver<CallTransactionResult, CallException> getCompleteReceiver(
+ String action, ResultReceiver callback) {
+ return new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
- Log.d(TAG, "addTransactionsToManager: onResult:");
+ public void onResult(CallTransactionResult result) {
+ Log.d(TAG, "completeReceiver: onResult[" + action + "]:" + result);
callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
}
@Override
public void onError(CallException exception) {
- Log.d(TAG, "addTransactionsToManager: onError");
+ Log.d(TAG, "completeReceiver: onError[" + action + "]" + exception);
Bundle extras = new Bundle();
extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
exception.getCode(), extras);
}
- });
+ };
}
public ICallControl getICallControl() {
@@ -365,122 +383,84 @@
**********************************************************************************************
*/
- public void onSetActive(Call call) {
+ public CompletableFuture<Boolean> onSetActive(Call call) {
+ CallTransaction callTransaction = new CallEventCallbackAckTransaction(
+ mICallEventCallback, ON_SET_ACTIVE, call.getId(), mLock);
+ CompletableFuture<Boolean> onSetActiveFuture;
try {
Log.startSession("TSW.oSA");
Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
- handleCallEventCallbackNewFocus(call, ON_SET_ACTIVE, false /*isAnswerRequest*/,
- 0 /*VideoState*/);
+ onSetActiveFuture = mCallSequencingAdapter.onSetActive(call,
+ callTransaction, result ->
+ Log.i(TAG, String.format(Locale.US,
+ "%s: onResult: callId=[%s], result=[%s]", ON_SET_ACTIVE,
+ call.getId(), result)));
} finally {
Log.endSession();
}
+ return onSetActiveFuture;
}
public void onAnswer(Call call, int videoState) {
try {
Log.startSession("TSW.oA");
Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
- handleCallEventCallbackNewFocus(call, ON_ANSWER, true /*isAnswerRequest*/,
- videoState /*VideoState*/);
+ mCallSequencingAdapter.onSetAnswered(call, videoState,
+ new CallEventCallbackAckTransaction(mICallEventCallback,
+ ON_ANSWER, call.getId(), videoState, mLock),
+ result -> Log.i(TAG, String.format(Locale.US,
+ "%s: onResult: callId=[%s], result=[%s]",
+ ON_ANSWER, call.getId(), result)));
} finally {
Log.endSession();
}
}
- // handle a CallEventCallback to set a call ACTIVE/ANSWERED. Must get ack from client since the
- // request has come from another source (ex. Android Auto is requesting a call to go active)
- private void handleCallEventCallbackNewFocus(Call call, String action, boolean isAnswerRequest,
- int potentiallyNewVideoState) {
- // save CallsManager state before sending client state changes
- Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
- boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
-
- SerialTransaction serialTransactions = createSetActiveTransactions(call);
- // 3. get ack from client (that the requested call can go active)
- if (isAnswerRequest) {
- serialTransactions.appendTransaction(
- new CallEventCallbackAckTransaction(mICallEventCallback,
- action, call.getId(), potentiallyNewVideoState, mLock));
- } else {
- serialTransactions.appendTransaction(
- new CallEventCallbackAckTransaction(mICallEventCallback,
- action, call.getId(), mLock));
- }
-
- // do CallsManager workload before asking client and
- // reset CallsManager state if client does NOT ack
- mTransactionManager.addTransaction(serialTransactions,
- new OutcomeReceiver<>() {
- @Override
- public void onResult(VoipCallTransactionResult result) {
- Log.i(TAG, String.format(Locale.US,
- "%s: onResult: callId=[%s]", action, call.getId()));
- if (isAnswerRequest) {
- call.setVideoState(potentiallyNewVideoState);
- }
- }
-
- @Override
- public void onError(CallException exception) {
- if (isAnswerRequest) {
- // This also sends the signal to untrack from TSW and the client_TSW
- removeCallFromCallsManager(call,
- new DisconnectCause(DisconnectCause.REJECTED,
- "client rejected to answer the call;"
- + " force disconnecting"));
- } else {
- mCallsManager.markCallAsOnHold(call);
- }
- maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
- }
- });
- }
-
-
- public void onSetInactive(Call call) {
+ public CompletableFuture<Boolean> onSetInactive(Call call) {
+ CallTransaction callTransaction = new CallEventCallbackAckTransaction(
+ mICallEventCallback, ON_SET_INACTIVE, call.getId(), mLock);
+ CompletableFuture<Boolean> onSetInactiveFuture;
try {
Log.startSession("TSW.oSI");
Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]", call.getId()));
- mTransactionManager.addTransaction(
- new CallEventCallbackAckTransaction(mICallEventCallback,
- ON_SET_INACTIVE, call.getId(), mLock), new OutcomeReceiver<>() {
+ onSetInactiveFuture = mCallSequencingAdapter.onSetInactive(call,
+ callTransaction, new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
- mCallsManager.markCallAsOnHold(call);
+ public void onResult(CallTransactionResult result) {
+ Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]"
+ + ", result=[%s]",
+ call.getId(), result));
}
@Override
public void onError(CallException exception) {
- Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception);
+ Log.w(TAG, "onSetInactive: onError: e.code=[%d], e.msg=[%s]",
+ exception.getCode(), exception.getMessage());
}
});
} finally {
Log.endSession();
}
+ return onSetInactiveFuture;
}
- public void onDisconnect(Call call, DisconnectCause cause) {
+ public CompletableFuture<Boolean> onDisconnect(Call call,
+ DisconnectCause cause) {
+ CallTransaction callTransaction = new CallEventCallbackAckTransaction(
+ mICallEventCallback, ON_DISCONNECT, call.getId(), cause, mLock);
+ CompletableFuture<Boolean> onDisconnectFuture;
try {
Log.startSession("TSW.oD");
Log.d(TAG, String.format(Locale.US, "onDisconnect: callId=[%s]", call.getId()));
-
- mTransactionManager.addTransaction(
- new CallEventCallbackAckTransaction(mICallEventCallback, ON_DISCONNECT,
- call.getId(), cause, mLock), new OutcomeReceiver<>() {
- @Override
- public void onResult(VoipCallTransactionResult result) {
- removeCallFromCallsManager(call, cause);
- }
-
- @Override
- public void onError(CallException exception) {
- removeCallFromCallsManager(call, cause);
- }
- }
- );
+ onDisconnectFuture = mCallSequencingAdapter.onSetDisconnected(call, cause,
+ callTransaction,
+ result -> Log.i(TAG, String.format(Locale.US,
+ "%s: onResult: callId=[%s], result=[%s]",
+ ON_DISCONNECT, call.getId(), result)));
} finally {
Log.endSession();
}
+ return onDisconnectFuture;
}
public void onCallStreamingStarted(Call call) {
@@ -493,13 +473,14 @@
new CallEventCallbackAckTransaction(mICallEventCallback, ON_STREAMING_STARTED,
call.getId(), mLock), new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
+ public void onResult(CallTransactionResult result) {
}
@Override
public void onError(CallException exception) {
- Log.i(TAG, "onCallStreamingStarted: onError: with e=[%e]",
- exception);
+ Log.w(TAG, "onCallStreamingStarted: onError: "
+ + "e.code=[%d], e.msg=[%s]",
+ exception.getCode(), exception.getMessage());
stopCallStreaming(call);
}
}
@@ -519,6 +500,7 @@
}
}
+ @Override
public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
if (call != null) {
try {
@@ -528,6 +510,7 @@
}
}
+ @Override
public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
if (call != null) {
try {
@@ -538,6 +521,7 @@
}
}
+ @Override
public void onMuteStateChanged(Call call, boolean isMuted) {
if (call != null) {
try {
@@ -547,6 +531,16 @@
}
}
+ @Override
+ public void onVideoStateChanged(Call call, int videoState) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onVideoStateChanged(call.getId(), videoState);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
public void removeCallFromWrappers(Call call) {
if (call != null) {
try {
@@ -559,7 +553,8 @@
}
}
- public void onEvent(Call call, String event, Bundle extras) {
+ @Override
+ public void sendCallEvent(Call call, String event, Bundle extras) {
if (call != null) {
try {
mICallEventCallback.onEvent(call.getId(), event, extras);
@@ -573,34 +568,6 @@
** Helpers **
**********************************************************************************************
*/
- private void maybeResetForegroundCall(Call foregroundCallBeforeSwap, boolean wasActive) {
- if (foregroundCallBeforeSwap == null) {
- return;
- }
- if (wasActive && !foregroundCallBeforeSwap.isActive()) {
- mCallsManager.markCallAsActive(foregroundCallBeforeSwap);
- }
- }
-
- private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
- if (cause.getCode() != DisconnectCause.REJECTED) {
- mCallsManager.markCallAsDisconnected(call, cause);
- }
- mCallsManager.removeCall(call);
- }
-
- private SerialTransaction createSetActiveTransactions(Call call) {
- // 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));
- // And request a new focus call update
- transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
-
- return new SerialTransaction(transactions, mLock);
- }
-
private void setDeathRecipient(ICallEventCallback callEventCallback) {
try {
callEventCallback.asBinder().linkToDeath(mAppDeathListener, 0);
@@ -651,9 +618,10 @@
public void stopCallStreaming(Call call) {
Log.i(this, "stopCallStreaming; callid=%s", call.getId());
if (call != null && call.isStreaming()) {
- VoipCallTransaction stopStreamingTransaction = mStreamingController
+ CallTransaction stopStreamingTransaction = mStreamingController
.getStopStreamingTransaction(call, mLock);
- addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
+ addTransactionsToManager(STOP_STREAMING, stopStreamingTransaction,
+ new ResultReceiver(null));
}
}
}
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index d0a561a..57906d4 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -24,8 +24,11 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
import com.android.server.telecom.components.ErrorDialogActivity;
+import com.android.server.telecom.flags.FeatureFlags;
public final class UserUtil {
@@ -33,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,
@@ -58,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");
@@ -68,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) {
@@ -99,4 +123,37 @@
}
return false;
}
+
+ /**
+ * Gets the associated user for the given call. Note: this is applicable to all calls except
+ * outgoing calls as the associated user is already based off of the user placing the
+ * call.
+ *
+ * @param phoneAccountRegistrar
+ * @param currentUser Current user profile (this can either be the admin or a secondary/guest
+ * user). Note that work profile users fall under the admin user.
+ * @param targetPhoneAccount The phone account to retrieve the {@link UserHandle} from.
+ * @return current user if it isn't the admin or if the work profile is paused for the target
+ * phone account handle user, otherwise return the target phone account handle user. If the
+ * flag is disabled, return the legacy {@link UserHandle}.
+ */
+ public static UserHandle getAssociatedUserForCall(boolean isAssociatedUserFlagEnabled,
+ PhoneAccountRegistrar phoneAccountRegistrar, UserHandle currentUser,
+ PhoneAccountHandle targetPhoneAccount) {
+ if (!isAssociatedUserFlagEnabled) {
+ return targetPhoneAccount.getUserHandle();
+ }
+ // For multi-user phone accounts, associate the call with the profile receiving/placing
+ // the call. For SIM accounts (that are assigned to specific users), the user association
+ // will be placed on the target phone account handle user.
+ PhoneAccount account = phoneAccountRegistrar.getPhoneAccountUnchecked(targetPhoneAccount);
+ if (account != null) {
+ return account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)
+ ? currentUser
+ : targetPhoneAccount.getUserHandle();
+ }
+ // If target phone account handle is null or account cannot be found,
+ // return the current user.
+ return currentUser;
+ }
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 20bca3d..dbc858b 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -16,6 +16,12 @@
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 static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -25,26 +31,38 @@
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
-import android.media.AudioManager;
import android.media.AudioDeviceInfo;
-import android.media.audio.common.AudioDevice;
+import android.media.AudioManager;
import android.os.Bundle;
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.concurrent.Executor;
+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;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
public class BluetoothDeviceManager {
@@ -52,6 +70,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
@@ -60,7 +88,8 @@
public void onGroupStatusChanged(int groupId, int groupStatus) {}
@Override
public void onGroupNodeAdded(BluetoothDevice device, int groupId) {
- Log.i(this, device.getAddress() + " group added " + groupId);
+ Log.i(this, (device == null ? "device is null" : device.getAddress())
+ + " group added " + groupId);
if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
Log.w(this, "invalid parameter");
return;
@@ -72,6 +101,8 @@
}
@Override
public void onGroupNodeRemoved(BluetoothDevice device, int groupId) {
+ Log.i(this, (device == null ? "device is null" : device.getAddress())
+ + " group removed " + groupId);
if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
Log.w(this, "invalid parameter");
return;
@@ -87,11 +118,14 @@
new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
- Log.startSession("BMSL.oSC");
+ Log.startSession("BPSL.oSC");
try {
synchronized (mLock) {
String logString;
if (profile == BluetoothProfile.HEADSET) {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture.complete((BluetoothHeadset) proxy);
+ }
mBluetoothHeadset = (BluetoothHeadset) proxy;
logString = "Got BluetoothHeadset: " + mBluetoothHeadset;
} else if (profile == BluetoothProfile.HEARING_AID) {
@@ -100,15 +134,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 {
@@ -123,14 +156,40 @@
}
}
+ 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("BMSL.oSD");
+ Log.startSession("BPSL.oSD");
try {
synchronized (mLock) {
LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
String logString;
if (profile == BluetoothProfile.HEADSET) {
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture.complete(null);
+ }
mBluetoothHeadset = null;
lostServiceDevices = mHfpDevicesByAddress;
mBluetoothRouteManager.onActiveDeviceChanged(null,
@@ -157,11 +216,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 {
@@ -170,6 +233,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, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+ }
+
private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
new LinkedHashMap<>();
private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -180,6 +271,12 @@
new LinkedHashMap<>();
private final LinkedHashMap<BluetoothDevice, Integer> mGroupsByDevice =
new LinkedHashMap<>();
+ private final ArrayList<LinkedHashMap<String, BluetoothDevice>>
+ mDevicesByAddressMaps = new ArrayList<LinkedHashMap<String, BluetoothDevice>>(); {
+ mDevicesByAddressMaps.add(mHfpDevicesByAddress);
+ mDevicesByAddressMaps.add(mHearingAidDevicesByAddress);
+ mDevicesByAddressMaps.add(mLeAudioDevicesByAddress);
+ }
private int mGroupIdActive = BluetoothLeAudio.GROUP_ID_INVALID;
private int mGroupIdPending = BluetoothLeAudio.GROUP_ID_INVALID;
private final LocalLog mLocalLog = new LocalLog(20);
@@ -189,6 +286,7 @@
private BluetoothRouteManager mBluetoothRouteManager;
private BluetoothHeadset mBluetoothHeadset;
+ private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
private BluetoothHearingAid mBluetoothHearingAid;
private boolean mLeAudioCallbackRegistered = false;
private BluetoothLeAudio mBluetoothLeAudioService;
@@ -200,8 +298,14 @@
private BluetoothAdapter mBluetoothAdapter;
private AudioManager mAudioManager;
private Executor mExecutor;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private CallAudioRouteAdapter mCallAudioRouteAdapter;
+ private FeatureFlags mFeatureFlags;
- public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter) {
+ public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
if (bluetoothAdapter != null) {
mBluetoothAdapter = bluetoothAdapter;
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -210,9 +314,13 @@
BluetoothProfile.HEARING_AID);
bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.LE_AUDIO);
- mAudioManager = context.getSystemService(AudioManager.class);
- mExecutor = context.getMainExecutor();
}
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothHeadsetFuture = new CompletableFuture<>();
+ }
+ mAudioManager = context.getSystemService(AudioManager.class);
+ mExecutor = context.getMainExecutor();
+ mCommunicationDeviceTracker = communicationDeviceTracker;
}
public void setBluetoothRouteManager(BluetoothRouteManager brm) {
@@ -315,7 +423,20 @@
}
public BluetoothHeadset getBluetoothHeadset() {
- return mBluetoothHeadset;
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ try {
+ mBluetoothHeadset = mBluetoothHeadsetFuture.get(500L,
+ TimeUnit.MILLISECONDS);
+ return mBluetoothHeadset;
+ } catch (TimeoutException | InterruptedException | ExecutionException e) {
+ // ignore
+ Log.w(this, "getBluetoothHeadset: Acquire BluetoothHeadset service failed due to: "
+ + e);
+ return null;
+ }
+ } else {
+ return mBluetoothHeadset;
+ }
}
public BluetoothAdapter getBluetoothAdapter() {
@@ -356,12 +477,14 @@
}
}
- void onDeviceConnected(BluetoothDevice device, int deviceType) {
+ @VisibleForTesting
+ public void onDeviceConnected(BluetoothDevice device, int deviceType) {
synchronized (mLock) {
+ clearDeviceFromDeviceMaps(device.getAddress());
LinkedHashMap<String, BluetoothDevice> targetDeviceMap;
if (deviceType == DEVICE_TYPE_LE_AUDIO) {
if (mBluetoothLeAudioService == null) {
- Log.w(this, "LE audio service null when receiving device added broadcast");
+ Log.w(this, "onDeviceConnected: LE audio service null");
return;
}
/* Check if group is known. */
@@ -375,32 +498,41 @@
targetDeviceMap = mLeAudioDevicesByAddress;
} else if (deviceType == DEVICE_TYPE_HEARING_AID) {
if (mBluetoothHearingAid == null) {
- Log.w(this, "Hearing aid service null when receiving device added broadcast");
+ Log.w(this, "onDeviceConnected: Hearing aid service null");
return;
}
long hiSyncId = mBluetoothHearingAid.getHiSyncId(device);
mHearingAidDeviceSyncIds.put(device, hiSyncId);
targetDeviceMap = mHearingAidDevicesByAddress;
} else if (deviceType == DEVICE_TYPE_HEADSET) {
- if (mBluetoothHeadset == null) {
- Log.w(this, "Headset service null when receiving device added broadcast");
+ if (getBluetoothHeadset() == null) {
+ Log.w(this, "onDeviceConnected: Headset service null");
return;
}
targetDeviceMap = mHfpDevicesByAddress;
} else {
- Log.w(this, "Device: " + device.getAddress() + " with invalid type: "
- + getDeviceTypeString(deviceType));
+ Log.w(this, "onDeviceConnected: Device: %s; invalid type %s", device.getAddress(),
+ getDeviceTypeString(deviceType));
return;
}
if (!targetDeviceMap.containsKey(device.getAddress())) {
- Log.i(this, "Adding device with address: " + device + " and devicetype="
- + getDeviceTypeString(deviceType));
+ Log.i(this, "onDeviceConnected: Adding device with address: %s and devicetype=%s",
+ device, getDeviceTypeString(deviceType));
targetDeviceMap.put(device.getAddress(), device);
- mBluetoothRouteManager.onDeviceAdded(device.getAddress());
+ if (!mFeatureFlags.keepBtDevicesCacheUpdated()
+ || !mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothRouteManager.onDeviceAdded(device.getAddress());
+ }
}
}
}
+ void clearDeviceFromDeviceMaps(String deviceAddress) {
+ for (LinkedHashMap<String, BluetoothDevice> deviceMap : mDevicesByAddressMaps) {
+ deviceMap.remove(deviceAddress);
+ }
+ }
+
void onDeviceDisconnected(BluetoothDevice device, int deviceType) {
mLocalLog.log("Device disconnected -- address: " + device.getAddress() + " deviceType: "
+ deviceType);
@@ -414,31 +546,42 @@
} else if (deviceType == DEVICE_TYPE_HEADSET) {
targetDeviceMap = mHfpDevicesByAddress;
} else {
- Log.w(this, "Device: " + device.getAddress() + " with invalid type: "
- + getDeviceTypeString(deviceType));
+ Log.w(this, "onDeviceDisconnected: Device: %s with invalid type: %s",
+ device.getAddress(), getDeviceTypeString(deviceType));
return;
}
if (targetDeviceMap.containsKey(device.getAddress())) {
- Log.i(this, "Removing device with address: " + device + " and devicetype="
- + getDeviceTypeString(deviceType));
+ Log.i(this, "onDeviceDisconnected: Removing device with address: %s, devicetype=%s",
+ device, getDeviceTypeString(deviceType));
targetDeviceMap.remove(device.getAddress());
- mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ if (!mFeatureFlags.keepBtDevicesCacheUpdated()
+ || !mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mBluetoothRouteManager.onDeviceLost(device.getAddress());
+ }
}
}
}
public void disconnectAudio() {
- disconnectSco();
- clearLeAudioCommunicationDevice();
- clearHearingAidCommunicationDevice();
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearBtCommunicationDevice();
+ disconnectSco();
+ } else {
+ disconnectSco();
+ clearLeAudioCommunicationDevice();
+ clearHearingAidCommunicationDevice();
+ }
}
- public void disconnectSco() {
- if (mBluetoothHeadset == null) {
- Log.w(this, "Trying to disconnect audio but no headset service exists.");
+ public int disconnectSco() {
+ int result = BluetoothStatusCodes.ERROR_UNKNOWN;
+ if (getBluetoothHeadset() == null) {
+ Log.w(this, "disconnectSco: Trying to disconnect audio but no headset service exists.");
} else {
- mBluetoothHeadset.disconnectAudio();
+ result = mBluetoothHeadset.disconnectAudio();
+ Log.i(this, "disconnectSco: BluetoothHeadset#disconnectAudio()=%b", result);
}
+ return result;
}
public boolean isLeAudioCommunicationDevice() {
@@ -470,6 +613,7 @@
if (audioDeviceInfo != null && audioDeviceInfo.getType()
== AudioDeviceInfo.TYPE_BLE_HEADSET) {
mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
+ Log.i(this, "clearLeAudioCommunicationDevice: audioManager#clearCommunicationDevice");
mAudioManager.clearCommunicationDevice();
}
}
@@ -494,32 +638,33 @@
AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice();
if (audioDeviceInfo != null && audioDeviceInfo.getType()
== AudioDeviceInfo.TYPE_HEARING_AID) {
+ Log.i(this, "clearHearingAidCommunicationDevice: "
+ + "audioManager#clearCommunicationDevice");
mAudioManager.clearCommunicationDevice();
}
}
public boolean setLeAudioCommunicationDevice() {
- Log.i(this, "setLeAudioCommunicationDevice");
-
if (mLeAudioSetAsCommunicationDevice) {
- Log.i(this, "setLeAudioCommunicationDevice already set");
+ Log.i(this, "setLeAudioCommunicationDevice: already set");
return true;
}
if (mAudioManager == null) {
- Log.w(this, " mAudioManager is null");
+ Log.w(this, "setLeAudioCommunicationDevice: mAudioManager is null");
return false;
}
AudioDeviceInfo bleHeadset = null;
List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
if (devices.size() == 0) {
- Log.w(this, " No communication devices available.");
+ Log.w(this, "setLeAudioCommunicationDevice: No communication devices available.");
return false;
}
for (AudioDeviceInfo device : devices) {
- Log.i(this, " Available device type: " + device.getType());
+ Log.d(this, "setLeAudioCommunicationDevice: Available device type: "
+ + device.getType());
if (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
bleHeadset = device;
break;
@@ -527,7 +672,7 @@
}
if (bleHeadset == null) {
- Log.w(this, " No bleHeadset device available");
+ Log.w(this, "setLeAudioCommunicationDevice: No bleHeadset device available");
return false;
}
@@ -537,9 +682,11 @@
// Turn BLE_OUT_HEADSET ON.
boolean result = mAudioManager.setCommunicationDevice(bleHeadset);
if (!result) {
- Log.w(this, " Could not set bleHeadset device");
+ Log.w(this, "setLeAudioCommunicationDevice: AudioManager#setCommunicationDevice(%s)=%b;"
+ + " Could not set bleHeadset device", bleHeadset, result);
} else {
- Log.i(this, " bleHeadset device set");
+ Log.i(this, "setLeAudioCommunicationDevice: "
+ + "AudioManager#setCommunicationDevice(%s)=%b", bleHeadset, result);
mBluetoothRouteManager.onAudioOn(bleHeadset.getAddress());
mLeAudioSetAsCommunicationDevice = true;
mLeAudioDevice = bleHeadset.getAddress();
@@ -548,27 +695,26 @@
}
public boolean setHearingAidCommunicationDevice() {
- Log.i(this, "setHearingAidCommunicationDevice");
-
if (mHearingAidSetAsCommunicationDevice) {
- Log.i(this, "mHearingAidSetAsCommunicationDevice already set");
+ Log.i(this, "setHearingAidCommunicationDevice: already set");
return true;
}
if (mAudioManager == null) {
- Log.w(this, " mAudioManager is null");
+ Log.w(this, "setHearingAidCommunicationDevice: mAudioManager is null");
return false;
}
AudioDeviceInfo hearingAid = null;
List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
if (devices.size() == 0) {
- Log.w(this, " No communication devices available.");
+ Log.w(this, "setHearingAidCommunicationDevice: No communication devices available.");
return false;
}
for (AudioDeviceInfo device : devices) {
- Log.i(this, " Available device type: " + device.getType());
+ Log.d(this, "setHearingAidCommunicationDevice: Available device type: "
+ + device.getType());
if (device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
hearingAid = device;
break;
@@ -576,7 +722,7 @@
}
if (hearingAid == null) {
- Log.w(this, " No hearingAid device available");
+ Log.w(this, "setHearingAidCommunicationDevice: No hearingAid device available");
return false;
}
@@ -586,57 +732,93 @@
// Turn hearing aid ON.
boolean result = mAudioManager.setCommunicationDevice(hearingAid);
if (!result) {
- Log.w(this, " Could not set hearingAid device");
+ Log.w(this, "setHearingAidCommunicationDevice: "
+ + "AudioManager#setCommunicationDevice(%s)=%b; Could not set HA device",
+ hearingAid, result);
} else {
- Log.i(this, " hearingAid device set");
+ Log.i(this, "setHearingAidCommunicationDevice: "
+ + "AudioManager#setCommunicationDevice(%s)=%b", hearingAid, result);
mHearingAidDevice = hearingAid.getAddress();
mHearingAidSetAsCommunicationDevice = true;
}
return result;
}
+ public boolean setCommunicationDeviceForAddress(String address) {
+ AudioDeviceInfo deviceInfo = null;
+ List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+ if (devices.size() == 0) {
+ Log.w(this, "setCommunicationDeviceForAddress: No communication devices available.");
+ return false;
+ }
+
+ for (AudioDeviceInfo device : devices) {
+ Log.d(this, "setCommunicationDeviceForAddress: Available device type: "
+ + device.getType());
+ if (device.getAddress().equals(address)) {
+ deviceInfo = device;
+ break;
+ }
+ }
+
+ if (deviceInfo == null) {
+ Log.w(this, "setCommunicationDeviceForAddress: Device %s not found.", address);
+ return false;
+ }
+ if (deviceInfo.equals(mAudioManager.getCommunicationDevice())) {
+ Log.i(this, "setCommunicationDeviceForAddress: Device %s already active.", address);
+ return true;
+ }
+ boolean success = mAudioManager.setCommunicationDevice(deviceInfo);
+ Log.i(this, "setCommunicationDeviceForAddress: "
+ + "AudioManager#setCommunicationDevice(%s)=%b", deviceInfo, success);
+ return success;
+ }
+
// 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) {
int callProfile = BluetoothProfile.LE_AUDIO;
- Log.i(this, "Telecomm connecting audio to device: " + address);
BluetoothDevice device = null;
if (mLeAudioDevicesByAddress.containsKey(address)) {
- Log.i(this, "Telecomm found LE Audio device for address: " + address);
+ Log.i(this, "connectAudio: found LE Audio device for address: %s", address);
if (mBluetoothLeAudioService == null) {
- Log.w(this, "Attempting to turn on audio when the le audio service is null");
+ Log.w(this, "connectAudio: Attempting to turn on audio when the le audio service "
+ + "is null");
return false;
}
device = mLeAudioDevicesByAddress.get(address);
callProfile = BluetoothProfile.LE_AUDIO;
} else if (mHearingAidDevicesByAddress.containsKey(address)) {
- Log.i(this, "Telecomm found hearing aid device for address: " + address);
if (mBluetoothHearingAid == null) {
- Log.w(this, "Attempting to turn on audio when the hearing aid service is null");
+ Log.w(this, "connectAudio: Attempting to turn on audio when the hearing aid "
+ + "service is null");
return false;
}
+ Log.i(this, "connectAudio: found hearing aid device for address: %s", address);
device = mHearingAidDevicesByAddress.get(address);
callProfile = BluetoothProfile.HEARING_AID;
} else if (mHfpDevicesByAddress.containsKey(address)) {
- Log.i(this, "Telecomm found HFP device for address: " + address);
- if (mBluetoothHeadset == null) {
- Log.w(this, "Attempting to turn on audio when the headset service is null");
+ if (getBluetoothHeadset() == null) {
+ Log.w(this, "connectAudio: Attempting to turn on audio when the headset service "
+ + "is null");
return false;
}
+ Log.i(this, "connectAudio: found HFP device for address: %s", address);
device = mHfpDevicesByAddress.get(address);
callProfile = BluetoothProfile.HEADSET;
}
if (device == null) {
- Log.w(this, "No active profiles for Bluetooth address=" + address);
+ Log.w(this, "No active profiles for Bluetooth address: %s", address);
return false;
}
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));
+ Log.i(this, "connectAudio: Preferred duplex profile for device=% is %d", address,
+ preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
}
@@ -647,7 +829,10 @@
* Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
* will be audio switched to is available to be choose as communication device */
if (!switchingBtDevices) {
- return setLeAudioCommunicationDevice();
+ return mFeatureFlags.callAudioCommunicationDeviceRefactor() ?
+ mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_BLE_HEADSET, device)
+ : setLeAudioCommunicationDevice();
}
return true;
}
@@ -658,7 +843,10 @@
* Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
* will be audio switched to is available to be choose as communication device */
if (!switchingBtDevices) {
- return setHearingAidCommunicationDevice();
+ return mFeatureFlags.callAudioCommunicationDeviceRefactor() ?
+ mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_HEARING_AID, null)
+ : setHearingAidCommunicationDevice();
}
return true;
}
@@ -667,14 +855,75 @@
boolean success = mBluetoothAdapter.setActiveDevice(device,
BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
if (!success) {
- Log.w(this, "Couldn't set active device to %s", address);
+ Log.w(this, "connectAudio: Couldn't set active device to %s", address);
return false;
}
- int scoConnectionRequest = mBluetoothHeadset.connectAudio();
- return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
- scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)", address);
+ if (getBluetoothHeadset() != null) {
+ int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+ Log.i(this, "connectAudio: BluetoothHeadset#connectAudio()=%d",
+ scoConnectionRequest);
+ return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+ scoConnectionRequest
+ == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ } else {
+ Log.w(this, "connectAudio: Couldn't find bluetooth headset service");
+ return false;
+ }
} else {
- Log.w(this, "Attempting to turn on audio for a disconnected device");
+ Log.w(this, "connectAudio: Attempting to turn on audio for disconnected device %s",
+ address);
+ return false;
+ }
+ }
+
+ /**
+ * 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, "connectAudio: Preferred duplex profile for device=%s is %d", address,
+ 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, "connectAudio: Couldn't set active device to %s", address);
+ return false;
+ }
+ if (getBluetoothHeadset() != null) {
+ int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+ Log.i(this, "connectaudio: BluetoothHeadset#connectAudio()=%d",
+ scoConnectionRequest);
+ return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+ scoConnectionRequest
+ == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+ } else {
+ Log.w(this, "connectAudio: Couldn't find bluetooth headset service");
+ return false;
+ }
+ } else {
+ Log.w(this, "connectAudio: Attempting to turn on audio for a disconnected device %s",
+ address);
return false;
}
}
@@ -694,29 +943,43 @@
if (mBluetoothHearingAidActiveDeviceCache != null) {
mBluetoothAdapter.setActiveDevice(mBluetoothHearingAidActiveDeviceCache,
BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ Log.i(this, "restoreHearingAidDevice: BluetoothAdapter#setActiveDevice(%s)",
+ mBluetoothHearingAidActiveDeviceCache.getAddress());
mBluetoothHearingAidActiveDeviceCache = null;
}
}
public boolean isInbandRingingEnabled() {
- BluetoothDevice activeDevice = mBluetoothRouteManager.getBluetoothAudioConnectedDevice();
- Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
- if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+ // Get the inband ringing enabled status of expected BT device to route call audio instead
+ // of using the address of currently connected device.
+ BluetoothDevice activeDevice = mBluetoothRouteManager.getMostRecentlyReportedActiveDevice();
+ return isInbandRingEnabled(activeDevice);
+ }
+
+ public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+ if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
if (mBluetoothLeAudioService == null) {
Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
return false;
}
- int groupId = mBluetoothLeAudioService.getGroupId(activeDevice);
+ int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice);
return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
} else {
- if (mBluetoothHeadset == null) {
+ if (getBluetoothHeadset() == null) {
Log.i(this, "isInbandRingingEnabled: no headset service available.");
return false;
}
- return mBluetoothHeadset.isInbandRingingEnabled();
+ boolean isEnabled = mBluetoothHeadset.isInbandRingingEnabled();
+ Log.i(this, "isInbandRingEnabled: device: %s, isEnabled: %b", bluetoothDevice,
+ isEnabled);
+ return isEnabled;
}
}
+ 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 2db81f1..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -24,9 +24,11 @@
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothLeAudio;
import android.content.Context;
+import android.media.AudioDeviceInfo;
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;
@@ -34,10 +36,11 @@
import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.FeatureFlags;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -133,7 +136,8 @@
@Override
public void enter() {
BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
- if (erroneouslyConnectedDevice != null) {
+ if (erroneouslyConnectedDevice != null &&
+ !erroneouslyConnectedDevice.equals(mHearingAidActiveDeviceCache)) {
Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
"Switching to audio-on state for that device.", erroneouslyConnectedDevice);
// change this to just transition to the new audio on state
@@ -162,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 {
@@ -178,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 {
@@ -251,6 +282,29 @@
SomeArgs args = (SomeArgs) msg.obj;
String address = (String) args.arg2;
boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+
+ if (switchingBtDevices) { // check if it is an hearing aid pair
+ BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
+ if (bluetoothAdapter != null) {
+ List<BluetoothDevice> activeHearingAids =
+ bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
+ for (BluetoothDevice hearingAid : activeHearingAids) {
+ if (hearingAid != null) {
+ String hearingAidAddress = hearingAid.getAddress();
+ if (hearingAidAddress != null) {
+ if (hearingAidAddress.equals(address) ||
+ hearingAidAddress.equals(mDeviceAddress)) {
+ switchingBtDevices = false;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+ switchingBtDevices &= (mDeviceAddress != null);
+ }
+ }
try {
switch (msg.what) {
case NEW_DEVICE_CONNECTED:
@@ -264,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 {
@@ -283,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 {
@@ -369,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:
@@ -381,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
@@ -389,11 +492,21 @@
break;
}
- String actualAddress = connectBtAudio(address,
- true /* switchingBtDevices*/);
- if (actualAddress != null) {
- transitionTo(getConnectingStateForAddress(address,
- "AudioConnected/CONNECT_BT"));
+ 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"));
+ } else {
+ transitionTo(getConnectingStateForAddress(address,
+ "AudioConnected/CONNECT_BT"));
+ }
} else {
Log.w(LOG_TAG, "Tried to connect to %s but failed" +
" to connect to any BT device.", (String) args.arg2);
@@ -403,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 {
@@ -470,15 +601,21 @@
private BluetoothDevice mHearingAidActiveDeviceCache = null;
private BluetoothDevice mLeAudioActiveDeviceCache = null;
private BluetoothDevice mMostRecentlyReportedActiveDevice = null;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private FeatureFlags mFeatureFlags;
public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
- BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
+ BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
super(BluetoothRouteManager.class.getSimpleName());
mContext = context;
mLock = lock;
mDeviceManager = deviceManager;
mDeviceManager.setBluetoothRouteManager(this);
mTimeoutsAdapter = timeoutsAdapter;
+ mCommunicationDeviceTracker = communicationDeviceTracker;
+ mFeatureFlags = featureFlags;
mAudioOffState = new AudioOffState();
addState(mAudioOffState);
@@ -622,12 +759,22 @@
if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
mLeAudioActiveDeviceCache = device;
if (device == null) {
- mDeviceManager.clearLeAudioCommunicationDevice();
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ } else {
+ mDeviceManager.clearLeAudioCommunicationDevice();
+ }
}
} else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
mHearingAidActiveDeviceCache = device;
if (device == null) {
- mDeviceManager.clearHearingAidCommunicationDevice();
+ if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(
+ AudioDeviceInfo.TYPE_HEARING_AID);
+ } else {
+ mDeviceManager.clearHearingAidCommunicationDevice();
+ }
}
} else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
mHfpActiveDeviceCache = device;
@@ -646,6 +793,10 @@
}
}
+ public BluetoothDevice getMostRecentlyReportedActiveDevice() {
+ return mMostRecentlyReportedActiveDevice;
+ }
+
public boolean hasBtActiveDevice() {
return mLeAudioActiveDeviceCache != null ||
mHearingAidActiveDeviceCache != null ||
@@ -691,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);
}
/**
@@ -705,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))
@@ -723,7 +991,7 @@
+ " Using arbitrary device - except watch");
if (deviceList.size() > 0) {
for (BluetoothDevice device : deviceList) {
- if (isWatch(device)) {
+ if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
Log.i(this, "Skipping a watch device: " + device);
continue;
}
@@ -834,21 +1102,37 @@
}
}
+ boolean isHearingAidSetForCommunication =
+ mFeatureFlags.callAudioCommunicationDeviceRefactor()
+ ? mCommunicationDeviceTracker.isAudioDeviceSetForType(
+ AudioDeviceInfo.TYPE_HEARING_AID)
+ : mDeviceManager.isHearingAidSetAsCommunicationDevice();
if (bluetoothHearingAid != null) {
- if (mDeviceManager.isHearingAidSetAsCommunicationDevice()) {
- for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
- BluetoothProfile.HEARING_AID)) {
- if (device != null) {
- hearingAidActiveDevice = device;
- activeDevices++;
- break;
+ if (isHearingAidSetForCommunication) {
+ List<BluetoothDevice> hearingAidsActiveDevices = bluetoothAdapter.getActiveDevices(
+ BluetoothProfile.HEARING_AID);
+ if (hearingAidsActiveDevices.contains(mHearingAidActiveDeviceCache)) {
+ hearingAidActiveDevice = mHearingAidActiveDeviceCache;
+ activeDevices++;
+ } else {
+ for (BluetoothDevice device : hearingAidsActiveDevices) {
+ if (device != null) {
+ hearingAidActiveDevice = device;
+ activeDevices++;
+ break;
+ }
}
}
}
}
+ boolean isLeAudioSetForCommunication =
+ mFeatureFlags.callAudioCommunicationDeviceRefactor()
+ ? mCommunicationDeviceTracker.isAudioDeviceSetForType(
+ AudioDeviceInfo.TYPE_BLE_HEADSET)
+ : mDeviceManager.isLeAudioCommunicationDevice();
if (bluetoothLeAudio != null) {
- if (mDeviceManager.isLeAudioCommunicationDevice()) {
+ if (isLeAudioSetForCommunication) {
for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
BluetoothProfile.LE_AUDIO)) {
if (device != null) {
@@ -890,6 +1174,11 @@
return mDeviceManager.isInbandRingingEnabled();
}
+ @VisibleForTesting
+ public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+ return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
+ }
+
private boolean addDevice(String address) {
if (mAudioConnectingStates.containsKey(address)) {
Log.i(this, "Attempting to add device %s twice.", address);
@@ -976,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 09b8f76..679db67 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,18 @@
package com.android.server.telecom.bluetooth;
+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.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
+import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -26,15 +38,19 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.media.AudioDeviceInfo;
import android.os.Bundle;
import android.telecom.Log;
import android.telecom.Logging.Session;
+import android.util.Pair;
import com.android.internal.os.SomeArgs;
-
-import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
-import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
-
+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();
@@ -56,6 +72,9 @@
private boolean mIsInCall = false;
private final BluetoothRouteManager mBluetoothRouteManager;
private final BluetoothDeviceManager mBluetoothDeviceManager;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private FeatureFlags mFeatureFlags;
+ private CallAudioRouteAdapter mCallAudioRouteAdapter;
public void onReceive(Context context, Intent intent) {
Log.startSession("BSR.oR");
@@ -101,14 +120,57 @@
args.arg2 = device.getAddress();
switch (bluetoothHeadsetAudioState) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
- if (!mIsInCall) {
- Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
- return;
+ 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");
+ return;
+ }
+ mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
}
- mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
- mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController =
+ (CallAudioRouteController) mCallAudioRouteAdapter;
+ audioRouteController.setIsScoAudioConnected(false);
+ if (audioRouteController.isPending()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+ device);
+ } else {
+ // Handle case where BT stack signals SCO disconnected but Telecom isn't
+ // processing any pending routes. This explicitly addresses cf instances
+ // where a remote device disconnects SCO. Telecom should ensure that audio
+ // is properly routed in the UI.
+ audioRouteController.getPendingAudioRoute()
+ .setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ INCLUDE_BLUETOOTH_IN_BASELINE, device.getAddress());
+ }
+ } else {
+ mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+ }
break;
}
}
@@ -126,12 +188,16 @@
}
int deviceType;
+ @AudioRoute.AudioRouteType int audioRouteType;
if (BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
+ audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
} else if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
+ audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
} else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
+ audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
} else {
Log.w(LOG_TAG, "handleConnectionStateChanged: %s invalid device type", device);
return;
@@ -142,10 +208,26 @@
device.getAddress(), bluetoothHeadsetState);
if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) {
- mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED,
+ audioRouteType, device);
+ if (mFeatureFlags.keepBtDevicesCacheUpdated()) {
+ mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+ }
+ } else {
+ mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+ }
} else if (bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTED
|| bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTING) {
- mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED,
+ audioRouteType, device);
+ if (mFeatureFlags.keepBtDevicesCacheUpdated()) {
+ mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+ }
+ } else {
+ mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+ }
}
}
@@ -154,12 +236,16 @@
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
int deviceType;
+ @AudioRoute.AudioRouteType int audioRouteType;
if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
+ audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
} else if (BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
+ audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
} else if (BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
+ audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
} else {
Log.w(LOG_TAG, "handleActiveDeviceChanged: %s invalid device type", device);
return;
@@ -168,62 +254,116 @@
Log.i(LOG_TAG, "Device %s is now the preferred BT device for %s", device,
BluetoothDeviceManager.getDeviceTypeString(deviceType));
- mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
- if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
- deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
- Session session = Log.createSubsession();
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = session;
+ if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+ CallAudioRouteController audioRouteController = (CallAudioRouteController)
+ mCallAudioRouteAdapter;
if (device == null) {
- mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+ // Update the active device cache immediately.
+ audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
+ mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
+ audioRouteType);
} else {
- if (!mIsInCall) {
- Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
- return;
- }
- args.arg2 = device.getAddress();
-
- boolean usePreferredAudioProfile = false;
- BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager.getBluetoothAdapter();
- int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
- if (bluetoothAdapter != null) {
- Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
- device);
- if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
- && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
- != 0) {
- Log.i(this, "Preferred duplex profile for device=" + device + " is "
- + preferredAudioProfiles.getInt(
- BluetoothAdapter.AUDIO_MODE_DUPLEX));
- usePreferredAudioProfile = true;
- preferredDuplexProfile =
- preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ // Update the active device cache immediately.
+ audioRouteController.updateActiveBluetoothDevice(
+ new Pair(audioRouteType, device.getAddress()));
+ 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.", device);
+ if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+ Log.i(this, "Sending PENDING_ROUTE_FAILED "
+ + "to pending audio route.");
+ mCallAudioRouteAdapter.getPendingAudioRoute()
+ .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+ device.getAddress()), device.getAddress());
+ } else {
+ Log.i(this, "Refrain from sending PENDING_ROUTE_FAILED"
+ + " to pending audio route.");
+ }
+ } 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);
}
}
-
- if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
- /* In Le Audio case, once device got Active, the Telecom needs to make sure it
- * is set as communication device before we can say that BT_AUDIO_IS_ON
- */
- if ((!usePreferredAudioProfile
- || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
- && !mBluetoothDeviceManager.setLeAudioCommunicationDevice()) {
- Log.w(LOG_TAG,
- "Device %s cannot be use as LE audio communication device.",
- device);
+ }
+ } else {
+ mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
+ if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
+ deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+ Session session = Log.createSubsession();
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = session;
+ if (device == null) {
+ mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+ } else {
+ if (!mIsInCall) {
+ Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
return;
}
- } else {
- /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
- if (!mBluetoothDeviceManager.setHearingAidCommunicationDevice()) {
- Log.w(LOG_TAG,
- "Device %s cannot be use as hearing aid communication device.",
+ args.arg2 = device.getAddress();
+
+ boolean usePreferredAudioProfile = false;
+ BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager
+ .getBluetoothAdapter();
+ int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
+ if (bluetoothAdapter != null) {
+ Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
device);
+ if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+ && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
+ != 0) {
+ Log.i(this, "Preferred duplex profile for device=" + device + " is "
+ + preferredAudioProfiles.getInt(
+ BluetoothAdapter.AUDIO_MODE_DUPLEX));
+ usePreferredAudioProfile = true;
+ preferredDuplexProfile =
+ preferredAudioProfiles.getInt(
+ BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ }
+ }
+
+ if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+ /* In Le Audio case, once device got Active, the Telecom needs to make sure
+ * it is set as communication device before we can say that BT_AUDIO_IS_ON
+ */
+ boolean isLeAudioSetForCommunication =
+ mFeatureFlags.callAudioCommunicationDeviceRefactor()
+ ? mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_BLE_HEADSET, device)
+ : mBluetoothDeviceManager.setLeAudioCommunicationDevice();
+ if ((!usePreferredAudioProfile
+ || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
+ && !isLeAudioSetForCommunication) {
+ Log.w(LOG_TAG,
+ "Device %s cannot be use as LE audio communication device.",
+ device);
+ }
} else {
- mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
+ boolean isHearingAidSetForCommunication =
+ mFeatureFlags.callAudioCommunicationDeviceRefactor()
+ ? mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_HEARING_AID, null)
+ : mBluetoothDeviceManager
+ .setHearingAidCommunicationDevice();
+ /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
+ if (!isHearingAidSetForCommunication) {
+ Log.w(LOG_TAG,
+ "Device %s cannot be use as hearing aid communication device.",
+ device);
+ } else {
+ mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
+ }
}
}
- }
+ }
}
}
@@ -232,12 +372,20 @@
}
public BluetoothStateReceiver(BluetoothDeviceManager deviceManager,
- BluetoothRouteManager routeManager) {
+ BluetoothRouteManager routeManager,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
mBluetoothDeviceManager = deviceManager;
mBluetoothRouteManager = routeManager;
+ mCommunicationDeviceTracker = communicationDeviceTracker;
+ mFeatureFlags = featureFlags;
}
public void setIsInCall(boolean isInCall) {
mIsInCall = isInCall;
}
+
+ public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+ mCallAudioRouteAdapter = adapter;
+ }
}
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
index a83f314..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,24 +39,35 @@
* 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 extras the extra attribute of the number.
+ * @param numberPresentation the presentation code associated with the call.
+ * @param isNumberInContacts indicates if the provided number exists as a contact.
* @return result code indicating if the number should be blocked, and if so why.
- * Valid values are: {@link BlockedNumberContract#STATUS_NOT_BLOCKED},
- * {@link BlockedNumberContract#STATUS_BLOCKED_IN_LIST},
- * {@link BlockedNumberContract#STATUS_BLOCKED_NOT_IN_CONTACTS},
- * {@link BlockedNumberContract#STATUS_BLOCKED_PAYPHONE},
- * {@link BlockedNumberContract#STATUS_BLOCKED_RESTRICTED},
- * {@link BlockedNumberContract#STATUS_BLOCKED_UNKNOWN_NUMBER}.
+ * Valid values are: {@link BlockCheckerFilter#STATUS_NOT_BLOCKED},
+ * {@link BlockCheckerFilter#STATUS_BLOCKED_IN_LIST},
+ * {@link BlockCheckerFilter#STATUS_BLOCKED_NOT_IN_CONTACTS},
+ * {@link BlockCheckerFilter#STATUS_BLOCKED_PAYPHONE},
+ * {@link BlockCheckerFilter#STATUS_BLOCKED_RESTRICTED},
+ * {@link BlockCheckerFilter#STATUS_BLOCKED_UNKNOWN_NUMBER}.
*/
- public int getBlockStatus(Context context, String phoneNumber, Bundle extras) {
+ public int getBlockStatus(Context context, String phoneNumber,
+ 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.SystemContract.shouldSystemBlockNumber(
- context, phoneNumber, extras);
+ 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/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
index 64060c8..7e3837d 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
@@ -21,6 +21,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.provider.CallLog;
import android.telecom.CallerInfo;
@@ -29,6 +30,7 @@
import com.android.server.telecom.Call;
import com.android.server.telecom.CallerInfoLookupHelper;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.LogUtils;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.settings.BlockedNumbersUtil;
@@ -45,12 +47,68 @@
private boolean mContactExists;
private HandlerThread mHandlerThread;
private Handler mHandler;
+ private FeatureFlags mFeatureFlags;
public static final long CALLER_INFO_QUERY_TIMEOUT = 5000;
+ /**
+ * Integer reason indicating whether a call was blocked, and if so why.
+ * @hide
+ */
+ public static final String RES_BLOCK_STATUS = "block_status";
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was not
+ * blocked.
+ * @hide
+ */
+ public static final int STATUS_NOT_BLOCKED = 0;
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+ * because it is in the list of blocked numbers maintained by the provider.
+ * @hide
+ */
+ public static final int STATUS_BLOCKED_IN_LIST = 1;
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+ * because it is from a restricted number.
+ * @hide
+ */
+ public static final int STATUS_BLOCKED_RESTRICTED = 2;
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+ * because it is from an unknown number.
+ * @hide
+ */
+ public static final int STATUS_BLOCKED_UNKNOWN_NUMBER = 3;
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+ * because it is from a pay phone.
+ * @hide
+ */
+ public static final int STATUS_BLOCKED_PAYPHONE = 4;
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+ * because it is from a number not in the users contacts.
+ * @hide
+ */
+ public static final int STATUS_BLOCKED_NOT_IN_CONTACTS = 5;
+
+ /**
+ * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+ * because it is from a number not available.
+ * @hide
+ */
+ public static final int STATUS_BLOCKED_UNAVAILABLE = 6;
+
public BlockCheckerFilter(Context context, Call call,
CallerInfoLookupHelper callerInfoLookupHelper,
- BlockCheckerAdapter blockCheckerAdapter) {
+ BlockCheckerAdapter blockCheckerAdapter, FeatureFlags featureFlags) {
mCall = call;
mContext = context;
mCallerInfoLookupHelper = callerInfoLookupHelper;
@@ -59,6 +117,7 @@
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
+ mFeatureFlags = featureFlags;
}
@Override
@@ -66,7 +125,13 @@
Log.addEvent(mCall, LogUtils.Events.BLOCK_CHECK_INITIATED);
CompletableFuture<CallFilteringResult> resultFuture = new CompletableFuture<>();
Bundle extras = new Bundle();
- if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(mContext)) {
+ final Context userContext;
+ if (mFeatureFlags.telecomMainUserInBlockCheck()) {
+ userContext = mContext.createContextAsUser(mCall.getAssociatedUser(), 0);
+ } else {
+ userContext = mContext;
+ }
+ if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(userContext)) {
int presentation = mCall.getHandlePresentation();
extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, presentation);
if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
@@ -77,7 +142,7 @@
if (info != null && info.contactExists) {
mContactExists = true;
}
- getBlockStatus(resultFuture);
+ getBlockStatus(resultFuture, userContext);
}
@Override
@@ -86,24 +151,31 @@
}
});
} else {
- getBlockStatus(resultFuture);
+ getBlockStatus(resultFuture, userContext);
}
} else {
- getBlockStatus(resultFuture);
+ getBlockStatus(resultFuture, userContext);
}
return resultFuture;
}
private void getBlockStatus(
- CompletableFuture<CallFilteringResult> resultFuture) {
- // Set extras
- Bundle extras = new Bundle();
- if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(mContext)) {
- int presentation = mCall.getHandlePresentation();
- extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, presentation);
- if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
- extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, mContactExists);
- }
+ CompletableFuture<CallFilteringResult> resultFuture, Context userContext) {
+ // Set presentation and if contact exists. Used in determining if the system should block
+ // the passed in number. Use default values as they would be returned if the keys didn't
+ // exist in the extras to maintain existing behavior.
+ int presentation;
+ boolean isNumberInContacts;
+ if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(userContext)) {
+ presentation = mCall.getHandlePresentation();
+ } else {
+ presentation = 0;
+ }
+
+ if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
+ isNumberInContacts = mContactExists;
+ } else {
+ isNumberInContacts = false;
}
// Set number
@@ -111,7 +183,8 @@
mCall.getHandle().getSchemeSpecificPart();
CompletableFuture.supplyAsync(
- () -> mBlockCheckerAdapter.getBlockStatus(mContext, number, extras),
+ () -> mBlockCheckerAdapter.getBlockStatus(userContext, number,
+ presentation, isNumberInContacts),
new LoggedHandlerExecutor(mHandler, "BCF.gBS", null))
.thenApplyAsync((x) -> completeResult(resultFuture, x),
new LoggedHandlerExecutor(mHandler, "BCF.gBS", null));
@@ -120,7 +193,7 @@
private int completeResult(CompletableFuture<CallFilteringResult> resultFuture,
int blockStatus) {
CallFilteringResult result;
- if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
+ if (blockStatus != STATUS_NOT_BLOCKED) {
result = new CallFilteringResult.Builder()
.setShouldAllowCall(false)
.setShouldReject(true)
@@ -143,8 +216,7 @@
.build();
}
Log.addEvent(mCall, LogUtils.Events.BLOCK_CHECK_FINISHED,
- BlockedNumberContract.SystemContract.blockStatusToString(blockStatus) + " "
- + result);
+ blockStatusToString(blockStatus) + " " + result);
resultFuture.complete(result);
mHandlerThread.quitSafely();
return blockStatus;
@@ -152,20 +224,20 @@
private int getBlockReason(int blockStatus) {
switch (blockStatus) {
- case BlockedNumberContract.STATUS_BLOCKED_IN_LIST:
+ case STATUS_BLOCKED_IN_LIST:
return CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER;
- case BlockedNumberContract.STATUS_BLOCKED_UNKNOWN_NUMBER:
- case BlockedNumberContract.STATUS_BLOCKED_UNAVAILABLE:
+ case STATUS_BLOCKED_UNKNOWN_NUMBER:
+ case STATUS_BLOCKED_UNAVAILABLE:
return CallLog.Calls.BLOCK_REASON_UNKNOWN_NUMBER;
- case BlockedNumberContract.STATUS_BLOCKED_RESTRICTED:
+ case STATUS_BLOCKED_RESTRICTED:
return CallLog.Calls.BLOCK_REASON_RESTRICTED_NUMBER;
- case BlockedNumberContract.STATUS_BLOCKED_PAYPHONE:
+ case STATUS_BLOCKED_PAYPHONE:
return CallLog.Calls.BLOCK_REASON_PAY_PHONE;
- case BlockedNumberContract.STATUS_BLOCKED_NOT_IN_CONTACTS:
+ case STATUS_BLOCKED_NOT_IN_CONTACTS:
return CallLog.Calls.BLOCK_REASON_NOT_IN_CONTACTS;
default:
@@ -174,4 +246,27 @@
return CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER;
}
}
+
+ /**
+ * Converts a block status constant to a string equivalent for logging.
+ */
+ private String blockStatusToString(int blockStatus) {
+ switch (blockStatus) {
+ case STATUS_NOT_BLOCKED:
+ return "not blocked";
+ case STATUS_BLOCKED_IN_LIST:
+ return "blocked - in list";
+ case STATUS_BLOCKED_RESTRICTED:
+ return "blocked - restricted";
+ case STATUS_BLOCKED_UNKNOWN_NUMBER:
+ return "blocked - unknown";
+ case STATUS_BLOCKED_PAYPHONE:
+ return "blocked - payphone";
+ case STATUS_BLOCKED_NOT_IN_CONTACTS:
+ return "blocked - not in contacts";
+ case STATUS_BLOCKED_UNAVAILABLE:
+ return "blocked - unavailable";
+ }
+ return "unknown";
+ }
}
diff --git a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
index b8658d8..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.SystemContract} 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/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index f07c0aa..efac87d 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -269,7 +269,8 @@
mContext = context;
mPackageManager = mContext.getPackageManager();
mCallsManager = callsManager;
- mAppName = appLabelProxy.getAppLabel(mPackageName);
+ mAppName = appLabelProxy.getAppLabel(mPackageName,
+ mCall.getAssociatedUser());
mParcelableCallUtilsConverter = parcelableCallUtilsConverter;
}
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
index d79e80e..a606a4d 100644
--- a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
@@ -27,6 +27,7 @@
import com.android.server.telecom.LogUtils;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.ArrayList;
import java.util.List;
@@ -55,6 +56,7 @@
private CallFilteringResult mCurrentResult;
private Context mContext;
private Timeouts.Adapter mTimeoutsAdapter;
+ private final FeatureFlags mFeatureFlags;
private class PostFilterTask {
private final CallFilter mFilter;
@@ -84,11 +86,12 @@
}
public IncomingCallFilterGraph(Call call, CallFilterResultCallback listener, Context context,
- Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock) {
+ Timeouts.Adapter timeoutsAdapter, FeatureFlags featureFlags,
+ TelecomSystem.SyncRoot lock) {
mListener = listener;
mCall = call;
mFiltersList = new ArrayList<>();
-
+ mFeatureFlags = featureFlags;
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
@@ -121,8 +124,8 @@
@Override
public void loggedRun() {
if (!mFinished) {
- Log.i(this, "Graph timed out when performing filtering.");
Log.addEvent(mCall, LogUtils.Events.FILTERING_TIMED_OUT);
+ mCurrentResult = onTimeoutCombineFinishedFilters(mFiltersList, mCurrentResult);
mListener.onCallFilteringComplete(mCall, mCurrentResult, true);
mFinished = true;
mHandlerThread.quit();
@@ -137,6 +140,28 @@
}.prepare(), mTimeoutsAdapter.getCallScreeningTimeoutMillis(mContext.getContentResolver()));
}
+ /**
+ * This helper takes all the call filters that were added to the graph, checks if filters have
+ * finished, and combines the results.
+ *
+ * @param filtersList all the CallFilters that were added to the call
+ * @param currentResult the current call filter result
+ * @return CallFilterResult of the combined finished Filters.
+ */
+ private CallFilteringResult onTimeoutCombineFinishedFilters(
+ List<CallFilter> filtersList,
+ CallFilteringResult currentResult) {
+ if (!mFeatureFlags.checkCompletedFiltersOnTimeout()) {
+ return currentResult;
+ }
+ for (CallFilter filter : filtersList) {
+ if (filter.result != null) {
+ currentResult = currentResult.combine(filter.result);
+ }
+ }
+ return currentResult;
+ }
+
private void scheduleFilter(CallFilter filter) {
CallFilteringResult result = new CallFilteringResult.Builder()
.setShouldAllowCall(true)
@@ -147,6 +172,9 @@
.setDndSuppressed(false)
.build();
for (CallFilter dependencyFilter : filter.getDependencies()) {
+ // When sequential nodes are completed, they are combined progressively.
+ // ex.) node_a --> node_b --> node_c
+ // node_a will combine with node_b before starting node_c
result = result.combine(dependencyFilter.getResult());
}
mCurrentResult = result;
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
new file mode 100644
index 0000000..4424178
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callfiltering;
+
+import android.content.Context;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.FeatureFlags;
+
+/**
+ * Interface to provide a {@link IncomingCallFilterGraph}. This class serve for unit test purpose
+ * to mock an incoming call filter graph in test code.
+ */
+public interface IncomingCallFilterGraphProvider {
+
+
+ /**
+ * Provide a {@link IncomingCallFilterGraph}
+ * @param call The call for the filters.
+ * @param listener Callback object to trigger when filtering is done.
+ * @param context An android context.
+ * @param timeoutsAdapter Adapter to provide timeout value for call filtering.
+ * @param featureFlags Telecom flags
+ * @param lock Telecom lock.
+ * @return
+ */
+ IncomingCallFilterGraph createGraph(Call call, CallFilterResultCallback listener,
+ Context context,
+ Timeouts.Adapter timeoutsAdapter,
+ FeatureFlags featureFlags,
+ TelecomSystem.SyncRoot lock);
+}
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/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
new file mode 100644
index 0000000..2f0ae45
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -0,0 +1,82 @@
+/*
+ * 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.callsequencing;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Controls the sequencing between calls when moving between the user ACTIVE (RINGING/ACTIVE) and
+ * user INACTIVE (INCOMING/HOLD/DISCONNECTED) states.
+ */
+public class CallSequencingController {
+// private final CallsManager mCallsManager;
+ private final TransactionManager mTransactionManager;
+// private final Handler mHandler;
+// private boolean mCallSequencingEnabled;
+
+ public CallSequencingController(CallsManager callsManager, boolean callSequencingEnabled) {
+// mCallsManager = callsManager;
+ mTransactionManager = TransactionManager.getInstance();
+ HandlerThread handlerThread = new HandlerThread(this.toString());
+ handlerThread.start();
+// mHandler = new Handler(handlerThread.getLooper());
+// mCallSequencingEnabled = callSequencingEnabled;
+ }
+
+ public void answerCall(Call incomingCall, int videoState) {
+ // Todo: call sequencing logic (stubbed)
+ }
+
+// private CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(Call call) {
+// // Todo: call sequencing logic (stubbed)
+// return null;
+// }
+
+ public void unholdCall(Call call) {
+ // Todo: call sequencing logic (stubbed)
+ }
+
+ public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
+ // Todo: call sequencing logic (stubbed)
+ return CompletableFuture.completedFuture(true);
+// return isEmergency ? makeRoomForOutgoingEmergencyCall(call) : makeRoomForOutgoingCall(call);
+ }
+
+// private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
+// // Todo: call sequencing logic (stubbed)
+// return CompletableFuture.completedFuture(true);
+// }
+
+// private CompletableFuture<Boolean> makeRoomForOutgoingCall(Call call) {
+// // Todo: call sequencing logic (stubbed)
+// return CompletableFuture.completedFuture(true);
+// }
+
+// private void resetProcessingCallSequencing() {
+// mTransactionManager.setProcessingCallSequencing(false);
+// }
+
+ public CompletableFuture<Boolean> disconnectCall() {
+ return CompletableFuture.completedFuture(true);
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/CallTransaction.java b/src/com/android/server/telecom/callsequencing/CallTransaction.java
new file mode 100644
index 0000000..8d7da7c
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/CallTransaction.java
@@ -0,0 +1,271 @@
+/*
+ * 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.callsequencing;
+
+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;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+
+public class CallTransaction {
+ //TODO: add log events
+ private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L;
+
+ /**
+ * Tracks stats about a transaction for logging purposes.
+ */
+ public static class Stats {
+ // the logging visible timestamp for ease of debugging
+ public final LocalDateTime addedTimeStamp;
+ // the time in nS that the transaction was first created
+ private final long mCreatedTimeNs;
+ // the time that the transaction was started.
+ private long mStartedTimeNs = -1L;
+ // the time that the transaction was finished.
+ private long mFinishedTimeNs = -1L;
+ // If finished, did this transaction finish because it timed out?
+ private boolean mIsTimedOut = false;
+ private CallTransactionResult mTransactionResult = null;
+
+ public Stats() {
+ addedTimeStamp = LocalDateTime.now();
+ mCreatedTimeNs = System.nanoTime();
+ }
+
+ /**
+ * Mark the transaction as started and record the time.
+ */
+ public void markStarted() {
+ if (mStartedTimeNs > -1) return;
+ mStartedTimeNs = System.nanoTime();
+ }
+
+ /**
+ * Mark the transaction as completed and record the time.
+ */
+ public void markComplete(boolean isTimedOut, CallTransactionResult result) {
+ if (mFinishedTimeNs > -1) return;
+ mFinishedTimeNs = System.nanoTime();
+ mIsTimedOut = isTimedOut;
+ mTransactionResult = result;
+ }
+
+ /**
+ * @return Time in mS since the transaction was created.
+ */
+ public long measureTimeSinceCreatedMs() {
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mCreatedTimeNs);
+ }
+
+ /**
+ * @return Time in mS between when transaction was created and when it was marked as
+ * started. Returns -1 if the transaction was not started yet.
+ */
+ public long measureCreatedToStartedMs() {
+ return mStartedTimeNs > 0 ?
+ TimeUnit.NANOSECONDS.toMillis(mStartedTimeNs - mCreatedTimeNs) : -1;
+ }
+
+ /**
+ * @return Time in mS since the transaction was marked started to the TransactionManager.
+ * Returns -1 if the transaction hasn't been started yet.
+ */
+ public long measureTimeSinceStartedMs() {
+ return mStartedTimeNs > 0 ?
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mStartedTimeNs) : -1;
+ }
+
+ /**
+ * @return Time in mS between when the transaction was marked as started and when it was
+ * marked as completed. Returns -1 if the transaction hasn't started or finished yet.
+ */
+ public long measureStartedToCompletedMs() {
+ return (mStartedTimeNs > 0 && mFinishedTimeNs > 0) ?
+ TimeUnit.NANOSECONDS.toMillis(mFinishedTimeNs - mStartedTimeNs) : -1;
+
+ }
+
+ /**
+ * @return true if this transaction completed due to timing out, false if the transaction
+ * hasn't completed yet or it completed and did not time out.
+ */
+ public boolean isTimedOut() {
+ return mIsTimedOut;
+ }
+
+ /**
+ * @return the result if the transaction completed, null if it timed out or hasn't completed
+ * yet.
+ */
+ public CallTransactionResult getTransactionResult() {
+ return mTransactionResult;
+ }
+ }
+
+ protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
+ protected final String mTransactionName = this.getClass().getSimpleName();
+ private final HandlerThread mHandlerThread;
+ protected final Handler mHandler;
+ protected TransactionManager.TransactionCompleteListener mCompleteListener;
+ protected final List<CallTransaction> mSubTransactions;
+ protected final TelecomSystem.SyncRoot mLock;
+ protected final long mTransactionTimeoutMs;
+ protected final Stats mStats;
+
+ public CallTransaction(
+ List<CallTransaction> 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 CallTransaction(List<CallTransaction> subTransactions,
+ TelecomSystem.SyncRoot lock) {
+ this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS);
+ }
+ public CallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
+ this(null /* mSubTransactions */, lock, timeoutMs);
+ }
+
+ public CallTransaction(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), mTransactionTimeoutMs);
+ future.thenApplyAsync((x) -> {
+ timeout();
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
+ + ".s", mLock));
+
+ processTransactions();
+ }
+
+ /**
+ * By default, this processes this transaction. For CallTransaction with sub-transactions,
+ * this implementation should be overwritten to handle also processing sub-transactions.
+ */
+ protected void processTransactions() {
+ scheduleTransaction();
+ }
+
+ /**
+ * This method is called when the transaction has finished either successfully or exceptionally.
+ * CallTransaction 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() + ".sT", mLock);
+ CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
+ future.thenComposeAsync(this::processTransaction, executor)
+ .thenApplyAsync((Function<CallTransactionResult, Void>) result -> {
+ notifyListenersOfResult(result);
+ return null;
+ }, 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 CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN, errorMessage));
+ Log.e(this, throwable, "Error while executing transaction.");
+ return null;
+ }));
+ }
+
+ protected void notifyListenersOfResult(CallTransactionResult result){
+ mCompleted.set(true);
+ finish(result);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionCompleted(result, mTransactionName);
+ }
+ }
+
+ protected CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ return CompletableFuture.completedFuture(
+ new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED, null));
+ }
+
+ public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
+ mCompleteListener = listener;
+ }
+
+ @VisibleForTesting
+ public final void timeout() {
+ if (mCompleted.getAndSet(true)) {
+ return;
+ }
+ finish(true, null);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionTimeout(mTransactionName);
+ }
+ }
+
+ @VisibleForTesting
+ public final Handler getHandler() {
+ return mHandler;
+ }
+
+ public final void finish(CallTransactionResult result) {
+ finish(false, result);
+ }
+
+ private void finish(boolean isTimedOut, CallTransactionResult 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.quitSafely();
+ }
+
+ /**
+ * @return Stats related to this transaction if stats are enabled, null otherwise.
+ */
+ public final Stats getStats() {
+ return mStats;
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/CallTransactionResult.java b/src/com/android/server/telecom/callsequencing/CallTransactionResult.java
new file mode 100644
index 0000000..8b5f5bf
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/CallTransactionResult.java
@@ -0,0 +1,88 @@
+/*
+ * 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.callsequencing;
+
+import com.android.server.telecom.Call;
+
+import java.util.Objects;
+
+public class CallTransactionResult {
+ public static final int RESULT_SUCCEED = 0;
+ private static final String VOIP_TRANSACTION_TAG = "VoipCallTransactionResult";
+ private static final String PSTN_TRANSACTION_TAG = "PstnTransactionResult";
+
+ // NOTE: if the CallTransactionResult 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;
+ private final Call mCall;
+ private final String mCallType;
+
+ public CallTransactionResult(int result, String message) {
+ mResult = result;
+ mMessage = message;
+ mCall = null;
+ mCallType = "";
+ }
+
+ public CallTransactionResult(int result, Call call, String message, boolean isVoip) {
+ mResult = result;
+ mCall = call;
+ mMessage = message;
+ mCallType = isVoip ? VOIP_TRANSACTION_TAG : PSTN_TRANSACTION_TAG;
+ }
+
+ public int getResult() {
+ return mResult;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+
+ public Call getCall(){
+ return mCall;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CallTransactionResult)) return false;
+ CallTransactionResult that = (CallTransactionResult) o;
+ return mResult == that.mResult && Objects.equals(mMessage, that.mMessage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mResult, mMessage);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder().
+ append("{ ").
+ append(mCallType).
+ append(": [mResult: ").
+ append(mResult).
+ append("], [mCall: ").
+ append((mCall != null) ? mCall : "null").
+ append("], [mMessage=").
+ append(mMessage).append("] }").toString();
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
new file mode 100644
index 0000000..8410c54
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * 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.callsequencing;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Abstraction layer for CallsManager to perform call sequencing operations through CallsManager
+ * or CallSequencingController, which is controlled by {@link FeatureFlags#enableCallSequencing()}.
+ */
+public class CallsManagerCallSequencingAdapter {
+
+ private final CallsManager mCallsManager;
+ private final CallSequencingController mSequencingController;
+ private final boolean mIsCallSequencingEnabled;
+
+ public CallsManagerCallSequencingAdapter(CallsManager callsManager,
+ CallSequencingController sequencingController,
+ boolean isCallSequencingEnabled) {
+ mCallsManager = callsManager;
+ mSequencingController = sequencingController;
+ mIsCallSequencingEnabled = isCallSequencingEnabled;
+ }
+
+ public void answerCall(Call incomingCall, int videoState) {
+ if (mIsCallSequencingEnabled && !incomingCall.isTransactionalCall()) {
+ mSequencingController.answerCall(incomingCall, videoState);
+ } else {
+ mCallsManager.answerCallOld(incomingCall, videoState);
+ }
+ }
+
+ public void unholdCall(Call call) {
+ if (mIsCallSequencingEnabled) {
+ mSequencingController.unholdCall(call);
+ } else {
+ mCallsManager.unholdCallOld(call);
+ }
+ }
+
+ public void holdCall(Call call) {
+ // Sequencing already taken care of for CSW/TSW in Call class.
+ call.hold();
+ }
+
+ public void unholdCallForRemoval(Call removedCall,
+ boolean isLocallyDisconnecting) {
+ // Todo: confirm verification of disconnect logic
+ // Sequencing already taken care of for CSW/TSW in Call class.
+ mCallsManager.maybeMoveHeldCallToForeground(removedCall, isLocallyDisconnecting);
+ }
+
+ public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
+ if (mIsCallSequencingEnabled) {
+ return mSequencingController.makeRoomForOutgoingCall(isEmergency, call);
+ } else {
+ return isEmergency
+ ? CompletableFuture.completedFuture(
+ makeRoomForOutgoingEmergencyCallFlagOff(call))
+ : CompletableFuture.completedFuture(makeRoomForOutgoingCallFlagOff(call));
+ }
+ }
+
+ private boolean makeRoomForOutgoingCallFlagOff(Call call) {
+ return mCallsManager.makeRoomForOutgoingCall(call);
+ }
+
+ private boolean makeRoomForOutgoingEmergencyCallFlagOff(Call call) {
+ return mCallsManager.makeRoomForOutgoingEmergencyCall(call);
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
new file mode 100644
index 0000000..a3b3828
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing;
+
+import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
+
+import android.os.OutcomeReceiver;
+import android.telecom.TelecomManager;
+import android.telecom.CallException;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.Flags;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Locale;
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+
+public class TransactionManager {
+ private static final String TAG = "CallTransactionManager";
+ private static final int TRANSACTION_HISTORY_SIZE = 20;
+ private static TransactionManager INSTANCE = null;
+ private static final Object sLock = new Object();
+ private final Queue<CallTransaction> mTransactions;
+ private final Deque<CallTransaction> mCompletedTransactions;
+ private CallTransaction mCurrentTransaction;
+ private boolean mProcessingCallSequencing;
+
+ public interface TransactionCompleteListener {
+ void onTransactionCompleted(CallTransactionResult result, String transactionName);
+ void onTransactionTimeout(String transactionName);
+ }
+
+ private TransactionManager() {
+ mTransactions = new ArrayDeque<>();
+ mCurrentTransaction = null;
+ if (Flags.enableCallSequencing()) {
+ mCompletedTransactions = new ArrayDeque<>();
+ } else
+ mCompletedTransactions = null;
+ }
+
+ public static TransactionManager getInstance() {
+ synchronized (sLock) {
+ if (INSTANCE == null) {
+ INSTANCE = new TransactionManager();
+ }
+ }
+ return INSTANCE;
+ }
+
+ @VisibleForTesting
+ public static TransactionManager getTestInstance() {
+ return new TransactionManager();
+ }
+
+ public CompletableFuture<Boolean> addTransaction(CallTransaction transaction,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ CompletableFuture<Boolean> transactionCompleteFuture = new CompletableFuture<>();
+ synchronized (sLock) {
+ mTransactions.add(transaction);
+ }
+ transaction.setCompleteListener(new TransactionCompleteListener() {
+ @Override
+ public void onTransactionCompleted(CallTransactionResult result,
+ String transactionName) {
+ Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
+ transactionName, result.getResult()));
+ try {
+ if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
+ receiver.onResult(result);
+ transactionCompleteFuture.complete(true);
+ } else {
+ receiver.onError(
+ new CallException(result.getMessage(),
+ result.getResult()));
+ transactionCompleteFuture.complete(false);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, String.format("onTransactionCompleted: Notifying transaction result"
+ + " %s resulted in an Exception.", result), e);
+ transactionCompleteFuture.complete(false);
+ }
+ finishTransaction();
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName){
+ Log.i(TAG, String.format("transaction %s timeout", transactionName));
+ try {
+ receiver.onError(new CallException(transactionName + " timeout",
+ CODE_OPERATION_TIMED_OUT));
+ transactionCompleteFuture.complete(false);
+ } catch (Exception e) {
+ Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
+ + " %s resulted in an Exception.", transactionName), e);
+ transactionCompleteFuture.complete(false);
+ }
+ finishTransaction();
+ }
+ });
+
+ startTransactions();
+ return transactionCompleteFuture;
+ }
+
+ private void startTransactions() {
+ synchronized (sLock) {
+ if (mTransactions.isEmpty()) {
+ // No transaction waiting for process
+ return;
+ }
+
+ if (mCurrentTransaction != null) {
+ // Ongoing transaction
+ return;
+ }
+ mCurrentTransaction = mTransactions.poll();
+ }
+ mCurrentTransaction.start();
+ }
+
+ private void finishTransaction() {
+ synchronized (sLock) {
+ if (mCurrentTransaction != null) {
+ addTransactionToHistory(mCurrentTransaction);
+ mCurrentTransaction = null;
+ }
+ }
+ startTransactions();
+ }
+
+ @VisibleForTesting
+ public void clear() {
+ List<CallTransaction> pendingTransactions;
+ synchronized (sLock) {
+ pendingTransactions = new ArrayList<>(mTransactions);
+ }
+ for (CallTransaction t : pendingTransactions) {
+ t.finish(new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN
+ /* TODO:: define error b/335703584 */, "clear called"));
+ }
+ }
+
+ private void addTransactionToHistory(CallTransaction t) {
+ if (!Flags.enableCallSequencing()) return;
+
+ mCompletedTransactions.add(t);
+ if (mCompletedTransactions.size() > TRANSACTION_HISTORY_SIZE) {
+ mCompletedTransactions.poll();
+ }
+ }
+
+ public void setProcessingCallSequencing(boolean processingCallSequencing) {
+ mProcessingCallSequencing = processingCallSequencing;
+ }
+
+ public boolean isProcessingCallSequencing() {
+ return mProcessingCallSequencing;
+ }
+
+ /**
+ * Called when the dumpsys is created for telecom to capture the current state.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ if (!Flags.enableCallSequencing()) {
+ pw.println("<<Flag not enabled>>");
+ return;
+ }
+ synchronized (sLock) {
+ pw.println("Pending Transactions:");
+ pw.increaseIndent();
+ for (CallTransaction t : mTransactions) {
+ printPendingTransactionStats(t, pw);
+ }
+ pw.decreaseIndent();
+
+ pw.println("Ongoing Transaction:");
+ pw.increaseIndent();
+ if (mCurrentTransaction != null) {
+ printPendingTransactionStats(mCurrentTransaction, pw);
+ }
+ pw.decreaseIndent();
+
+ pw.println("Completed Transactions:");
+ pw.increaseIndent();
+ for (CallTransaction t : mCompletedTransactions) {
+ printCompleteTransactionStats(t, pw);
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ /**
+ * Recursively print the pending {@link CallTransaction} stats for logging purposes.
+ * @param t The transaction that stats should be printed for
+ * @param pw The IndentingPrintWriter to print the result to
+ */
+ private void printPendingTransactionStats(CallTransaction t, IndentingPrintWriter pw) {
+ CallTransaction.Stats s = t.getStats();
+ if (s == null) {
+ pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+ return;
+ }
+ pw.println(String.format(Locale.getDefault(),
+ "[%s] %s: (result=[%s]), (created -> now : [%+d] mS),"
+ + " (created -> started : [%+d] mS),"
+ + " (started -> now : [%+d] mS)",
+ s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+ s.measureTimeSinceCreatedMs(), s.measureCreatedToStartedMs(),
+ s.measureTimeSinceStartedMs()));
+
+ if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+ return;
+ }
+ pw.increaseIndent();
+ for (CallTransaction subTransaction : t.mSubTransactions) {
+ printPendingTransactionStats(subTransaction, pw);
+ }
+ pw.decreaseIndent();
+ }
+
+ /**
+ * Recursively print the complete Transaction stats for logging purposes.
+ * @param t The transaction that stats should be printed for
+ * @param pw The IndentingPrintWriter to print the result to
+ */
+ private void printCompleteTransactionStats(CallTransaction t, IndentingPrintWriter pw) {
+ CallTransaction.Stats s = t.getStats();
+ if (s == null) {
+ pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+ return;
+ }
+ pw.println(String.format(Locale.getDefault(),
+ "[%s] %s: (result=[%s]), (created -> started : [%+d] mS), "
+ + "(started -> completed : [%+d] mS)",
+ s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+ s.measureCreatedToStartedMs(), s.measureStartedToCompletedMs()));
+
+ if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+ return;
+ }
+ pw.increaseIndent();
+ for (CallTransaction subTransaction : t.mSubTransactions) {
+ printCompleteTransactionStats(subTransaction, pw);
+ }
+ pw.decreaseIndent();
+ }
+
+ private String parseTransactionResult(CallTransaction.Stats s) {
+ if (s.isTimedOut()) return "TIMED OUT";
+ if (s.getTransactionResult() == null) return "PENDING";
+ if (s.getTransactionResult().getResult() == CallTransactionResult.RESULT_SUCCEED) {
+ return "SUCCESS";
+ }
+ return s.getTransactionResult().toString();
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
new file mode 100644
index 0000000..7c8bbe4
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -0,0 +1,304 @@
+/*
+ * 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.callsequencing;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
+import com.android.server.telecom.callsequencing.voip.HoldCallTransaction;
+import com.android.server.telecom.callsequencing.voip.MaybeHoldCallForNewCallTransaction;
+import com.android.server.telecom.callsequencing.voip.RequestNewActiveCallTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Helper adapter class used to centralized code that will be affected by toggling the
+ * {@link Flags#enableCallSequencing()} flag.
+ */
+public class TransactionalCallSequencingAdapter {
+ private final TransactionManager mTransactionManager;
+ private final CallsManager mCallsManager;
+// private final boolean mIsCallSequencingEnabled;
+
+ public TransactionalCallSequencingAdapter(TransactionManager transactionManager,
+ CallsManager callsManager, boolean isCallSequencingEnabled) {
+ mTransactionManager = transactionManager;
+ mCallsManager = callsManager;
+ // TODO implement call sequencing changes
+// mIsCallSequencingEnabled = isCallSequencingEnabled;
+ }
+
+ /**
+ * Client -> Server request to set a call active
+ */
+ public void setActive(Call call,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ setActiveFlagOff(call, receiver);
+ }
+
+ /**
+ * Client -> Server request to answer a call
+ */
+ public void setAnswered(Call call, int newVideoState,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ setAnsweredFlagOff(call, newVideoState, receiver);
+ }
+
+ /**
+ * Client -> Server request to set a call to disconnected
+ */
+ public void setDisconnected(Call call, DisconnectCause dc,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ setDisconnectedFlagOff(call, dc, receiver);
+ }
+
+ /**
+ * Client -> Server request to set a call to inactive
+ */
+ public void setInactive(Call call,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ setInactiveFlagOff(call, receiver);
+ }
+
+ /**
+ * Server -> Client command to set the call active, which if it fails, will try to reset the
+ * state to what it was before the call was set to active.
+ */
+ public CompletableFuture<Boolean> onSetActive(Call call,
+ CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ return onSetActiveFlagOff(call, clientCbT, receiver);
+ }
+
+ /**
+ * Server -> Client command to answer an incoming call, which if it fails, will trigger the
+ * disconnect of the call and then reset the state of the other call back to what it was before.
+ */
+ public void onSetAnswered(Call call, int videoState, CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ onSetAnsweredFlagOff(call, videoState, clientCbT, receiver);
+ }
+
+ /**
+ * Server -> Client command to set the call as inactive
+ */
+ public CompletableFuture<Boolean> onSetInactive(Call call,
+ CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ return onSetInactiveFlagOff(call, clientCbT, receiver);
+ }
+
+ /**
+ * Server -> Client command to disconnect the call
+ */
+ public CompletableFuture<Boolean> onSetDisconnected(Call call,
+ DisconnectCause dc, CallTransaction clientCbT, OutcomeReceiver<CallTransactionResult,
+ CallException> receiver) {
+ return onSetDisconnectedFlagOff(call, dc, clientCbT, receiver);
+ }
+
+ /**
+ * Clean up the calls that have been passed in from CallsManager
+ */
+ public void cleanup(Collection<Call> calls) {
+ cleanupFlagOff(calls);
+ }
+
+ private void setActiveFlagOff(Call call,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ CompletableFuture<Boolean> transactionResult = mTransactionManager
+ .addTransaction(createSetActiveTransactions(call,
+ true /* callControlRequest */), receiver);
+ }
+
+ private void setAnsweredFlagOff(Call call, int newVideoState,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ CompletableFuture<Boolean> transactionResult = mTransactionManager
+ .addTransaction(createSetActiveTransactions(call,
+ true /* callControlRequest */),
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult callTransactionResult) {
+ call.setVideoState(newVideoState);
+ receiver.onResult(callTransactionResult);
+ }
+
+ @Override
+ public void onError(CallException error) {
+ receiver.onError(error);
+ }
+ });
+ }
+
+ private void setDisconnectedFlagOff(Call call, DisconnectCause dc,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ CompletableFuture<Boolean> transactionResult = mTransactionManager
+ .addTransaction(new EndCallTransaction(mCallsManager,
+ dc, call), receiver);
+ }
+
+ private void setInactiveFlagOff(Call call,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ CompletableFuture<Boolean> transactionResult = mTransactionManager
+ .addTransaction(new HoldCallTransaction(mCallsManager,call), receiver);
+ }
+
+ private CompletableFuture<Boolean> onSetActiveFlagOff(Call call,
+ CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ // save CallsManager state before sending client state changes
+ Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+ boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+ SerialTransaction serialTransactions = createSetActiveTransactions(call,
+ false /* callControlRequest */);
+ serialTransactions.appendTransaction(clientCbT);
+ // do CallsManager workload before asking client and
+ // reset CallsManager state if client does NOT ack
+ return mTransactionManager.addTransaction(
+ serialTransactions,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ receiver.onResult(result);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ mCallsManager.markCallAsOnHold(call);
+ maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
+ receiver.onError(exception);
+ }
+ });
+ }
+
+ private void onSetAnsweredFlagOff(Call call, int videoState, CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ // save CallsManager state before sending client state changes
+ Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+ boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+ SerialTransaction serialTransactions = createSetActiveTransactions(call,
+ false /* callControlRequest */);
+ serialTransactions.appendTransaction(clientCbT);
+ // do CallsManager workload before asking client and
+ // reset CallsManager state if client does NOT ack
+ CompletableFuture<Boolean> transactionResult = mTransactionManager
+ .addTransaction(serialTransactions,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ call.setVideoState(videoState);
+ receiver.onResult(result);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ // This also sends the signal to untrack from TSW and the client_TSW
+ removeCallFromCallsManager(call,
+ new DisconnectCause(DisconnectCause.REJECTED,
+ "client rejected to answer the call;"
+ + " force disconnecting"));
+ maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
+ receiver.onError(exception);
+ }
+ });
+ }
+
+ private CompletableFuture<Boolean> onSetInactiveFlagOff(Call call,
+ CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ return mTransactionManager.addTransaction(clientCbT,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult callTransactionResult) {
+ mCallsManager.markCallAsOnHold(call);
+ receiver.onResult(callTransactionResult);
+ }
+
+ @Override
+ public void onError(CallException error) {
+ receiver.onError(error);
+ }
+ });
+ }
+
+ /**
+ * Server -> Client command to disconnect the call
+ */
+ private CompletableFuture<Boolean> onSetDisconnectedFlagOff(Call call,
+ DisconnectCause dc, CallTransaction clientCbT,
+ OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+ return mTransactionManager.addTransaction(clientCbT,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ removeCallFromCallsManager(call, dc);
+ receiver.onResult(result);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ removeCallFromCallsManager(call, dc);
+ receiver.onError(exception);
+ }
+ }
+ );
+ }
+
+ private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
+ // create list for multiple transactions
+ List<CallTransaction> transactions = new ArrayList<>();
+
+ // potentially hold the current active call in order to set a new call (active/answered)
+ transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call,
+ isCallControlRequest));
+ // And request a new focus call update
+ transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
+
+ return new SerialTransaction(transactions, mCallsManager.getLock());
+ }
+
+ private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
+ if (cause.getCode() != DisconnectCause.REJECTED) {
+ mCallsManager.markCallAsDisconnected(call, cause);
+ }
+ mCallsManager.removeCall(call);
+ }
+
+ private void maybeResetForegroundCall(Call foregroundCallBeforeSwap, boolean wasActive) {
+ if (foregroundCallBeforeSwap == null) {
+ return;
+ }
+ if (wasActive && !foregroundCallBeforeSwap.isActive()) {
+ mCallsManager.markCallAsActive(foregroundCallBeforeSwap);
+ }
+ }
+ private void cleanupFlagOff(Collection<Call> calls) {
+ for (Call call : calls) {
+ mCallsManager.markCallAsDisconnected(call,
+ new DisconnectCause(DisconnectCause.ERROR, "process died"));
+ mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
new file mode 100644
index 0000000..82b32fb
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+
+import android.telecom.Log;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * VerifyCallStateChangeTransaction is a transaction that verifies a CallState change and has
+ * the ability to disconnect if the CallState is not changed within the timeout window.
+ * <p>
+ * Note: This transaction has a timeout of 2 seconds.
+ */
+public class VerifyCallStateChangeTransaction extends CallTransaction {
+ private static final String TAG = VerifyCallStateChangeTransaction.class.getSimpleName();
+ private static final long CALL_STATE_TIMEOUT_MILLISECONDS = 2000L;
+ private final Call mCall;
+ private final Set<Integer> mTargetCallStates;
+ private final CompletableFuture<CallTransactionResult> mTransactionResult =
+ new CompletableFuture<>();
+
+ private final Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() {
+ @Override
+ public void onCallStateChanged(int newCallState) {
+ Log.d(TAG, "newState=[%d], possible expected state(s)=[%s]", newCallState,
+ mTargetCallStates);
+ if (mTargetCallStates.contains(newCallState)) {
+ mTransactionResult.complete(new CallTransactionResult(
+ CallTransactionResult.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(TelecomSystem.SyncRoot lock, Call call,
+ int... targetCallStates) {
+ super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
+ mCall = call;
+ mTargetCallStates = IntStream.of(targetCallStates).boxed().collect(Collectors.toSet());;
+ }
+
+ @Override
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction:");
+ // It's possible the Call is already in the expected call state
+ if (isNewCallStateTargetCallState()) {
+ mTransactionResult.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, TAG));
+ return mTransactionResult;
+ }
+ mCall.addCallStateListener(mCallStateListenerImpl);
+ return mTransactionResult;
+ }
+
+ @Override
+ public void finishTransaction() {
+ mCall.removeCallStateListener(mCallStateListenerImpl);
+ }
+
+ private boolean isNewCallStateTargetCallState() {
+ return mTargetCallStates.contains(mCall.getState());
+ }
+
+ @VisibleForTesting
+ public CompletableFuture<CallTransactionResult> getTransactionResult() {
+ return mTransactionResult;
+ }
+
+ @VisibleForTesting
+ public Call.CallStateListener getCallStateListenerImpl() {
+ return mCallStateListenerImpl;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
similarity index 89%
rename from src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
index 93d9836..802ea7e 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
@@ -29,6 +29,8 @@
import com.android.internal.telecom.ICallEventCallback;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.TransactionalServiceWrapper;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -39,7 +41,7 @@
* SRP: using the ICallEventCallback binder, reach out to the client for the pending call event and
* get an acknowledgement that the call event can be completed.
*/
-public class CallEventCallbackAckTransaction extends VoipCallTransaction {
+public class CallEventCallbackAckTransaction extends CallTransaction {
private static final String TAG = CallEventCallbackAckTransaction.class.getSimpleName();
private final ICallEventCallback mICallEventCallback;
private final String mAction;
@@ -48,7 +50,7 @@
private int mVideoState = CallAttributes.AUDIO_CALL;
private DisconnectCause mDisconnectCause = null;
- private final VoipCallTransactionResult TRANSACTION_FAILED = new VoipCallTransactionResult(
+ private final CallTransactionResult TRANSACTION_FAILED = new CallTransactionResult(
CODE_OPERATION_TIMED_OUT, "failed to complete the operation before timeout");
private static class AckResultReceiver extends ResultReceiver {
@@ -96,7 +98,7 @@
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
CountDownLatch latch = new CountDownLatch(1);
ResultReceiver receiver = new AckResultReceiver(latch);
@@ -125,7 +127,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:"
@@ -134,7 +136,7 @@
} else {
// success
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
"success"));
}
} catch (InterruptedException ie) {
diff --git a/src/com/android/server/telecom/voip/EndCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/EndCallTransaction.java
similarity index 82%
rename from src/com/android/server/telecom/voip/EndCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/EndCallTransaction.java
index 0cb7458..b4c92fe 100644
--- a/src/com/android/server/telecom/voip/EndCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/EndCallTransaction.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import android.telecom.DisconnectCause;
import android.util.Log;
@@ -22,6 +22,8 @@
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -29,7 +31,7 @@
/**
* This transaction should only be created for a CallControl action.
*/
-public class EndCallTransaction extends VoipCallTransaction {
+public class EndCallTransaction extends CallTransaction {
private static final String TAG = EndCallTransaction.class.getSimpleName();
private final CallsManager mCallsManager;
private final Call mCall;
@@ -43,7 +45,7 @@
}
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
int code = mCause.getCode();
Log.d(TAG, String.format("processTransaction: mCode=[%d], mCall=[%s]", code, mCall));
@@ -56,7 +58,7 @@
mCallsManager.markCallAsRemoved(mCall);
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
"EndCallTransaction: RESULT_SUCCEED"));
}
}
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/callsequencing/voip/EndpointChangeTransaction.java
similarity index 68%
rename from src/com/android/server/telecom/voip/EndpointChangeTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/EndpointChangeTransaction.java
index e037a79..46678da 100644
--- a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/EndpointChangeTransaction.java
@@ -14,19 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
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;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-public class EndpointChangeTransaction extends VoipCallTransaction {
+public class EndpointChangeTransaction extends CallTransaction {
private static final String TAG = EndpointChangeTransaction.class.getSimpleName();
private final CallEndpoint mCallEndpoint;
private final CallsManager mCallsManager;
@@ -38,19 +41,20 @@
}
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.i(TAG, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
mCallsManager.requestCallEndpointChange(mCallEndpoint, new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
Log.i(TAG, "processTransaction: code=" + resultCode);
if (resultCode == CallEndpoint.ENDPOINT_OPERATION_SUCCESS) {
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
} else {
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED, null));
+ // TODO:: define errors in CallException class. b/335703584
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN, null));
}
}
});
diff --git a/src/com/android/server/telecom/voip/HoldCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/HoldCallTransaction.java
similarity index 72%
rename from src/com/android/server/telecom/voip/HoldCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/HoldCallTransaction.java
index 6c4e8b7..2fa7ff7 100644
--- a/src/com/android/server/telecom/voip/HoldCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/HoldCallTransaction.java
@@ -14,18 +14,20 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import android.telecom.CallException;
import android.util.Log;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-public class HoldCallTransaction extends VoipCallTransaction {
+public class HoldCallTransaction extends CallTransaction {
private static final String TAG = HoldCallTransaction.class.getSimpleName();
private final CallsManager mCallsManager;
@@ -38,17 +40,17 @@
}
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
if (mCallsManager.canHold(mCall)) {
mCallsManager.markCallAsOnHold(mCall);
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
} else {
Log.d(TAG, "processTransaction: onError");
- future.complete(new VoipCallTransactionResult(
+ future.complete(new CallTransactionResult(
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL, "cannot hold call"));
}
return future;
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/IncomingCallTransaction.java
similarity index 62%
rename from src/com/android/server/telecom/voip/IncomingCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/IncomingCallTransaction.java
index d35030c..31ce303 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/IncomingCallTransaction.java
@@ -14,47 +14,60 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
+import static com.android.server.telecom.callsequencing.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.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-public class IncomingCallTransaction extends VoipCallTransaction {
+public class IncomingCallTransaction extends CallTransaction {
private static final String TAG = IncomingCallTransaction.class.getSimpleName();
private final String mCallId;
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
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
if (mCallsManager.isIncomingCallPermitted(mCallAttributes.getPhoneAccountHandle())) {
@@ -65,22 +78,31 @@
generateExtras(mCallAttributes), false);
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, call, "success"));
+ new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, call, "success", true));
} else {
Log.d(TAG, "processTransaction: incoming call is not permitted at this time");
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(
+ new CallTransactionResult(
CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
"incoming call not permitted at the current time"));
}
}
- 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/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
similarity index 62%
rename from src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
index a245c1c..32062b5 100644
--- a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import android.os.OutcomeReceiver;
import android.telecom.CallException;
@@ -22,39 +22,49 @@
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
+/**
+ * This VOIP CallTransaction 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 CallTransaction {
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
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> 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");
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
}
@Override
public void onError(CallException exception) {
Log.d(TAG, "processTransaction: onError");
- future.complete(new VoipCallTransactionResult(
+ future.complete(new CallTransactionResult(
exception.getCode(), exception.getMessage()));
}
});
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
similarity index 67%
rename from src/com/android/server/telecom/voip/OutgoingCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
index b2625e6..572de55 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
@@ -14,13 +14,16 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import static android.Manifest.permission.CALL_PRIVILEGED;
import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
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.callsequencing.voip.VideoStateTranslation
+ .TransactionalVideoStateToVideoProfileState;
+
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -29,14 +32,18 @@
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.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-public class OutgoingCallTransaction extends VoipCallTransaction {
+public class OutgoingCallTransaction extends CallTransaction {
private static final String TAG = OutgoingCallTransaction.class.getSimpleName();
private final String mCallId;
@@ -45,9 +52,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,15 +67,16 @@
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
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
@@ -85,11 +98,11 @@
if (callFuture == null) {
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(
+ new CallTransactionResult(
CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
"incoming call not permitted at the current time"));
}
- CompletionStage<VoipCallTransactionResult> result = callFuture.thenComposeAsync(
+ CompletionStage<CallTransactionResult> result = callFuture.thenComposeAsync(
(call) -> {
Log.d(TAG, "processTransaction: completing future");
@@ -97,36 +110,50 @@
if (call == null) {
Log.d(TAG, "processTransaction: call is null");
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(
+ new CallTransactionResult(
CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
"call could not be created at this time"));
} else {
Log.d(TAG, "processTransaction: call done. id=" + call.getId());
}
+ if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) {
+ // set to dialing so the CallAnomalyWatchdog gives the VoIP calls 1
+ // minute to timeout rather than 5 seconds.
+ mCallsManager.markCallAsDialing(call);
+ }
+
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED,
- call, null));
+ new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED,
+ call, null, true));
}
, new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
return result;
} else {
return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(
+ new CallTransactionResult(
CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
"incoming call not permitted at the current time"));
}
}
- 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/callsequencing/voip/ParallelTransaction.java b/src/com/android/server/telecom/callsequencing/voip/ParallelTransaction.java
new file mode 100644
index 0000000..77e93f9
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/ParallelTransaction.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing.voip;
+
+import android.telecom.CallException;
+
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A CallTransaction implementation that its sub transactions will be executed in parallel
+ */
+public class ParallelTransaction extends CallTransaction {
+ public ParallelTransaction(List<CallTransaction> subTransactions,
+ TelecomSystem.SyncRoot lock) {
+ super(subTransactions, lock);
+ }
+
+ @Override
+ public void processTransactions() {
+ if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+ scheduleTransaction();
+ return;
+ }
+ TransactionManager.TransactionCompleteListener subTransactionListener =
+ new TransactionManager.TransactionCompleteListener() {
+ private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
+
+ @Override
+ public void onTransactionCompleted(CallTransactionResult result,
+ String transactionName) {
+ if (result.getResult() != CallTransactionResult.RESULT_SUCCEED) {
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ finish(result);
+ mCompleteListener.onTransactionCompleted(result,
+ mTransactionName);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTC", mLock));
+ } else {
+ if (mCount.decrementAndGet() == 0) {
+ scheduleTransaction();
+ }
+ }
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName) {
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ CallTransactionResult mainResult =
+ new CallTransactionResult(
+ 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 (CallTransaction transaction : mSubTransactions) {
+ transaction.setCompleteListener(subTransactionListener);
+ transaction.start();
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/RequestNewActiveCallTransaction.java
similarity index 79%
rename from src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/RequestNewActiveCallTransaction.java
index f586cc3..8e6e354 100644
--- a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/RequestNewActiveCallTransaction.java
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
import android.os.OutcomeReceiver;
-import android.telecom.CallAttributes;
import android.telecom.CallException;
import android.util.Log;
@@ -25,6 +24,9 @@
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.Flags;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -42,7 +44,7 @@
* - MaybeHoldCallForNewCallTransaction was performed before this so any potential active calls
* should be held now.
*/
-public class RequestNewActiveCallTransaction extends VoipCallTransaction {
+public class RequestNewActiveCallTransaction extends CallTransaction {
private static final String TAG = RequestNewActiveCallTransaction.class.getSimpleName();
private final CallsManager mCallsManager;
@@ -55,22 +57,23 @@
}
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
Log.d(TAG, "processTransaction");
- CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
int currentCallState = mCall.getState();
// certain calls cannot go active/answered (ex. disconnect calls, etc.)
if (!canBecomeNewCallFocus(currentCallState)) {
- future.complete(new VoipCallTransactionResult(
+ future.complete(new CallTransactionResult(
CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
"CallState cannot be set to active or answered due to current call"
+ " state being in invalid state"));
return future;
}
- if (mCallsManager.getActiveCall() != null) {
- future.complete(new VoipCallTransactionResult(
+ if (!Flags.transactionalHoldDisconnectsUnholdable() &&
+ mCallsManager.getActiveCall() != null) {
+ future.complete(new CallTransactionResult(
CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
"Already an active call. Request hold on current active call."));
return future;
@@ -80,14 +83,14 @@
@Override
public void onResult(Boolean result) {
Log.d(TAG, "processTransaction: onResult");
- future.complete(new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_SUCCEED, null));
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED, null));
}
@Override
public void onError(CallException exception) {
Log.d(TAG, "processTransaction: onError");
- future.complete(new VoipCallTransactionResult(
+ future.complete(new CallTransactionResult(
exception.getCode(), exception.getMessage()));
}
});
diff --git a/src/com/android/server/telecom/callsequencing/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/callsequencing/voip/RequestVideoStateTransaction.java
new file mode 100644
index 0000000..6fb1836
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/RequestVideoStateTransaction.java
@@ -0,0 +1,69 @@
+/*
+ * 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.callsequencing.voip;
+
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+ .TransactionalVideoStateToVideoProfileState;
+
+import android.telecom.CallException;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RequestVideoStateTransaction extends CallTransaction {
+
+ private static final String TAG = RequestVideoStateTransaction.class.getSimpleName();
+ private final Call mCall;
+ private final int mVideoProfileState;
+
+ public RequestVideoStateTransaction(CallsManager callsManager, Call call,
+ int transactionalVideoState) {
+ super(callsManager.getLock());
+ mCall = call;
+ mVideoProfileState = TransactionalVideoStateToVideoProfileState(transactionalVideoState);
+ }
+
+ @Override
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
+
+ if (isRequestingVideoTransmission(mVideoProfileState) &&
+ !mCall.isVideoCallingSupportedByPhoneAccount()) {
+ future.complete(new CallTransactionResult(
+ CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
+ "Video calling is not supported by the target account"));
+ } else {
+ mCall.setVideoState(mVideoProfileState);
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED,
+ "The Video State was changed successfully"));
+ }
+ return future;
+ }
+
+ private boolean isRequestingVideoTransmission(int targetVideoState) {
+ return targetVideoState != VideoProfile.STATE_AUDIO_ONLY;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/callsequencing/voip/SerialTransaction.java b/src/com/android/server/telecom/callsequencing/voip/SerialTransaction.java
new file mode 100644
index 0000000..d5d75d0
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/SerialTransaction.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing.voip;
+
+import android.telecom.CallException;
+
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A CallTransaction implementation that its sub transactions will be executed in serial
+ */
+public class SerialTransaction extends CallTransaction {
+ public SerialTransaction(List<CallTransaction> subTransactions,
+ TelecomSystem.SyncRoot lock) {
+ super(subTransactions, lock);
+ }
+
+ public void appendTransaction(CallTransaction transaction){
+ mSubTransactions.add(transaction);
+ }
+
+ @Override
+ public void processTransactions() {
+ if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+ scheduleTransaction();
+ return;
+ }
+ TransactionManager.TransactionCompleteListener subTransactionListener =
+ new TransactionManager.TransactionCompleteListener() {
+ private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
+
+ @Override
+ public void onTransactionCompleted(CallTransactionResult result,
+ String transactionName) {
+ if (result.getResult() != CallTransactionResult.RESULT_SUCCEED) {
+ handleTransactionFailure();
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ finish(result);
+ mCompleteListener.onTransactionCompleted(result,
+ mTransactionName);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler,
+ mTransactionName + "@" + hashCode()
+ + ".oTC", mLock));
+ } else {
+ int currTransactionIndex = mTransactionIndex.incrementAndGet();
+ if (currTransactionIndex < mSubTransactions.size()) {
+ CallTransaction transaction = mSubTransactions.get(
+ currTransactionIndex);
+ transaction.setCompleteListener(this);
+ transaction.start();
+ } else {
+ scheduleTransaction();
+ }
+ }
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName) {
+ handleTransactionFailure();
+ CompletableFuture.completedFuture(null).thenApplyAsync(
+ (x) -> {
+ CallTransactionResult mainResult =
+ new CallTransactionResult(
+ 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));
+ }
+ };
+ CallTransaction transaction = mSubTransactions.get(0);
+ transaction.setCompleteListener(subTransactionListener);
+ transaction.start();
+
+ }
+
+ public void handleTransactionFailure() {}
+}
diff --git a/src/com/android/server/telecom/callsequencing/voip/SetMuteStateTransaction.java b/src/com/android/server/telecom/callsequencing/voip/SetMuteStateTransaction.java
new file mode 100644
index 0000000..14f8945
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/SetMuteStateTransaction.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.callsequencing.voip;
+
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * This transaction should be used to change the global mute state for transactional
+ * calls. There is currently no way for this transaction to fail.
+ */
+public class SetMuteStateTransaction extends CallTransaction {
+
+ private static final String TAG = SetMuteStateTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final boolean mIsMuted;
+
+ public SetMuteStateTransaction(CallsManager callsManager, boolean isMuted) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ mIsMuted = isMuted;
+ }
+
+ @Override
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
+
+ mCallsManager.mute(mIsMuted);
+
+ future.complete(new CallTransactionResult(
+ CallTransactionResult.RESULT_SUCCEED,
+ "The Mute State was changed successfully"));
+
+ return future;
+ }
+}
diff --git a/src/com/android/server/telecom/callsequencing/voip/VideoStateTranslation.java b/src/com/android/server/telecom/callsequencing/voip/VideoStateTranslation.java
new file mode 100644
index 0000000..4610f96
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/VideoStateTranslation.java
@@ -0,0 +1,110 @@
+/*
+ * 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.callsequencing.voip;
+
+import android.telecom.CallAttributes;
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+
+/**
+ * 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}.
+ * To be more specific, there are 3 video states (rx, tx, and bi-directional).
+ * {@link CallAttributes.CallType} only has 2 states (audio and video).
+ *
+ * The reason why Transactional calls have fewer states is due to the fact that the framework is
+ * only used by VoIP apps and Telecom only cares to know if the call is audio or video.
+ *
+ * Calls that are backed by a {@link android.telecom.ConnectionService} have the ability to be
+ * managed calls (non-VoIP) and Dialer needs more fine grain video states to update the UI. Thus,
+ * {@link VideoProfile} is used for {@link android.telecom.ConnectionService} backed calls.
+ */
+public class VideoStateTranslation {
+ private static final String TAG = VideoStateTranslation.class.getSimpleName();
+
+ /**
+ * Client --> Telecom
+ * This should be used when the client application is signaling they are changing the video
+ * state.
+ */
+ 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 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;
+ }
+ }
+
+ /**
+ * Telecom --> Client
+ * This should be used when Telecom is informing the client of a video state change.
+ */
+ public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
+ 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;
+ }
+ }
+ }
+
+ public static String TransactionalVideoStateToString(int transactionalVideoState) {
+ if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
+ return "CallAttributes.AUDIO_CALL";
+ } else if (transactionalVideoState == CallAttributes.VIDEO_CALL) {
+ return "CallAttributes.VIDEO_CALL";
+ } else {
+ return "CallAttributes.UNKNOWN";
+ }
+ }
+
+ private static String VideoProfileStateToString(int videoProfileState) {
+ switch (videoProfileState) {
+ case VideoProfile.STATE_BIDIRECTIONAL -> {
+ return "VideoProfile.STATE_BIDIRECTIONAL";
+ }
+ case VideoProfile.STATE_RX_ENABLED -> {
+ return "VideoProfile.STATE_RX_ENABLED";
+ }
+ case VideoProfile.STATE_TX_ENABLED -> {
+ return "VideoProfile.STATE_TX_ENABLED";
+ }
+ case VideoProfile.STATE_AUDIO_ONLY -> {
+ return "VideoProfile.STATE_AUDIO_ONLY";
+ }
+ default -> {
+ return "VideoProfile.UNKNOWN";
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
similarity index 94%
rename from src/com/android/server/telecom/voip/VoipCallMonitor.java
rename to src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
index 3779a6d..1d1a1a6 100644
--- a/src/com/android/server/telecom/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
@@ -14,7 +14,13 @@
* limitations under the License.
*/
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
+
+import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
@@ -199,8 +205,11 @@
ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
false /* isSticky */, String.valueOf(handle.hashCode()),
- 0 /* foregroundServiceType */,
- ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL);
+ FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+ FOREGROUND_SERVICE_TYPE_MICROPHONE |
+ FOREGROUND_SERVICE_TYPE_CAMERA |
+ FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+ DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
ServiceConnection fgsConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
diff --git a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
index 3a0d517..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();
}
}
@@ -74,7 +79,7 @@
* @param packageName The name of the removed package.
*/
private void handlePackageRemoved(Context context, String packageName) {
- final TelecomManager telecomManager = TelecomManager.from(context);
+ final TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
if (telecomManager != null) {
telecomManager.clearAccountsForPackage(packageName);
}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 90a683f..4db3e14 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;
@@ -45,6 +46,7 @@
import com.android.server.telecom.ContactsAsyncHelper;
import com.android.server.telecom.DefaultDialerCache;
import com.android.server.telecom.DeviceIdleControllerAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.HeadsetMediaButton;
import com.android.server.telecom.HeadsetMediaButtonFactory;
import com.android.server.telecom.InCallWakeLockControllerFactory;
@@ -61,6 +63,7 @@
import com.android.server.telecom.TelecomWakeLock;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
import com.android.server.telecom.settings.BlockedNumbersUtil;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.MissedCallNotifierImpl;
@@ -78,10 +81,11 @@
Log.d(this, "onBind");
return new ITelecomLoader.Stub() {
@Override
- public ITelecomService createTelecomService(IInternalServiceRetriever retriever) {
+ public ITelecomService createTelecomService(IInternalServiceRetriever retriever,
+ String sysUiPackageName) {
InternalServiceRetrieverAdapter adapter =
new InternalServiceRetrieverAdapter(retriever);
- initializeTelecomSystem(TelecomService.this, adapter);
+ initializeTelecomSystem(TelecomService.this, adapter, sysUiPackageName);
synchronized (getTelecomSystem().getLock()) {
return getTelecomSystem().getTelecomServiceImpl().getBinder();
}
@@ -100,8 +104,9 @@
* @param context
*/
static void initializeTelecomSystem(Context context,
- InternalServiceRetrieverAdapter internalServiceRetriever) {
+ InternalServiceRetrieverAdapter internalServiceRetriever, String sysUiPackageName) {
if (TelecomSystem.getInstance() == null) {
+ FeatureFlags featureFlags = new FeatureFlagsImpl();
NotificationChannelManager notificationChannelManager =
new NotificationChannelManager();
notificationChannelManager.createChannels(context);
@@ -115,10 +120,11 @@
Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
- DeviceIdleControllerAdapter idleControllerAdapter) {
+ DeviceIdleControllerAdapter idleControllerAdapter,
+ FeatureFlags featureFlags) {
return new MissedCallNotifierImpl(context,
phoneAccountRegistrar, defaultDialerCache,
- idleControllerAdapter);
+ idleControllerAdapter, featureFlags);
}
},
new CallerInfoAsyncQueryFactory() {
@@ -199,6 +205,7 @@
(RoleManager) context.getSystemService(Context.ROLE_SERVICE)),
new ContactsAsyncHelper.Factory(),
internalServiceRetriever.getDeviceIdleController(),
+ sysUiPackageName,
new Ringer.AccessibilityManagerAdapter() {
@Override
public boolean startFlashNotificationSequence(
@@ -220,8 +227,11 @@
@Override
public boolean shouldShowEmergencyCallNotification(Context
context) {
- return BlockedNumberContract.SystemContract
- .shouldShowEmergencyCallNotification(context);
+ return featureFlags.telecomMainlineBlockedNumbersManager()
+ ? context.getSystemService(BlockedNumbersManager.class)
+ .shouldShowEmergencyCallNotification()
+ : BlockedNumberContract.SystemContract
+ .shouldShowEmergencyCallNotification(context);
}
@Override
@@ -230,7 +240,9 @@
BlockedNumbersUtil.updateEmergencyCallNotification(context,
showNotification);
}
- }));
+ },
+ 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/metrics/ApiStats.java b/src/com/android/server/telecom/metrics/ApiStats.java
new file mode 100644
index 0000000..4b23e47
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/ApiStats.java
@@ -0,0 +1,365 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class ApiStats extends TelecomPulledAtom {
+ public static final int API_UNSPECIFIC = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_UNSPECIFIED;
+ public static final int API_ACCEPTHANDOVER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ACCEPT_HANDOVER;
+ public static final int API_ACCEPTRINGINGCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ACCEPT_RINGING_CALL;
+ public static final int API_ACCEPTRINGINGCALLWITHVIDEOSTATE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ACCEPT_RINGING_CALL_WITH_VIDEO_STATE;
+ public static final int API_ADDCALL = TelecomStatsLog.TELECOM_API_STATS__API_NAME__API_ADD_CALL;
+ public static final int API_ADDNEWINCOMINGCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ADD_NEW_INCOMING_CALL;
+ public static final int API_ADDNEWINCOMINGCONFERENCE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ADD_NEW_INCOMING_CONFERENCE;
+ public static final int API_ADDNEWUNKNOWNCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ADD_NEW_UNKNOWN_CALL;
+ public static final int API_CANCELMISSEDCALLSNOTIFICATION = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_CANCEL_MISSED_CALLS_NOTIFICATION;
+ public static final int API_CLEARACCOUNTS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_CLEAR_ACCOUNTS;
+ public static final int API_CREATELAUNCHEMERGENCYDIALERINTENT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_CREATE_LAUNCH_EMERGENCY_DIALER_INTENT;
+ public static final int API_CREATEMANAGEBLOCKEDNUMBERSINTENT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_CREATE_MANAGE_BLOCKED_NUMBERS_INTENT;
+ public static final int API_DUMP = TelecomStatsLog.TELECOM_API_STATS__API_NAME__API_DUMP;
+ public static final int API_DUMPCALLANALYTICS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_DUMP_CALL_ANALYTICS;
+ public static final int API_ENABLEPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_ENABLE_PHONE_ACCOUNT;
+ public static final int API_ENDCALL = TelecomStatsLog.TELECOM_API_STATS__API_NAME__API_END_CALL;
+ public static final int API_GETADNURIFORPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_ADN_URI_FOR_PHONE_ACCOUNT;
+ public static final int API_GETALLPHONEACCOUNTHANDLES = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_ALL_PHONE_ACCOUNT_HANDLES;
+ public static final int API_GETALLPHONEACCOUNTS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_ALL_PHONE_ACCOUNTS;
+ public static final int API_GETALLPHONEACCOUNTSCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_ALL_PHONE_ACCOUNTS_COUNT;
+ public static final int API_GETCALLCAPABLEPHONEACCOUNTS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_CALL_CAPABLE_PHONE_ACCOUNTS;
+ public static final int API_GETCALLSTATE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_CALL_STATE;
+ public static final int API_GETCALLSTATEUSINGPACKAGE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_CALL_STATE_USING_PACKAGE;
+ public static final int API_GETCURRENTTTYMODE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_CURRENT_TTY_MODE;
+ public static final int API_GETDEFAULTDIALERPACKAGE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_DIALER_PACKAGE;
+ public static final int API_GETDEFAULTDIALERPACKAGEFORUSER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_DIALER_PACKAGE_FOR_USER;
+ public static final int API_GETDEFAULTOUTGOINGPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_OUTGOING_PHONE_ACCOUNT;
+ public static final int API_GETDEFAULTPHONEAPP = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_PHONE_APP;
+ public static final int API_GETLINE1NUMBER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_LINE1_NUMBER;
+ public static final int API_GETOWNSELFMANAGEDPHONEACCOUNTS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_OWN_SELF_MANAGED_PHONE_ACCOUNTS;
+ public static final int API_GETPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_PHONE_ACCOUNT;
+ public static final int API_GETPHONEACCOUNTSFORPACKAGE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_PHONE_ACCOUNTS_FOR_PACKAGE;
+ public static final int API_GETPHONEACCOUNTSSUPPORTINGSCHEME = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_PHONE_ACCOUNTS_SUPPORTING_SCHEME;
+ public static final int API_GETREGISTEREDPHONEACCOUNTS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_REGISTERED_PHONE_ACCOUNTS;
+ public static final int API_GETSELFMANAGEDPHONEACCOUNTS = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_SELF_MANAGED_PHONE_ACCOUNTS;
+ public static final int API_GETSIMCALLMANAGER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_SIM_CALL_MANAGER;
+ public static final int API_GETSIMCALLMANAGERFORUSER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_SIM_CALL_MANAGER_FOR_USER;
+ public static final int API_GETSYSTEMDIALERPACKAGE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_SYSTEM_DIALER_PACKAGE;
+ public static final int API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT;
+ public static final int API_GETVOICEMAILNUMBER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_GET_VOICE_MAIL_NUMBER;
+ public static final int API_HANDLEPINMMI = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_HANDLE_PIN_MMI;
+ public static final int API_HANDLEPINMMIFORPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_HANDLE_PIN_MMI_FOR_PHONE_ACCOUNT;
+ public static final int API_HASMANAGEONGOINGCALLSPERMISSION = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_HAS_MANAGE_ONGOING_CALLS_PERMISSION;
+ public static final int API_ISINCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_IN_CALL;
+ public static final int API_ISINCOMINGCALLPERMITTED = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_IN_EMERGENCY_CALL;
+ public static final int API_ISINEMERGENCYCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_IN_MANAGED_CALL;
+ public static final int API_ISINMANAGEDCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_IN_SELF_MANAGED_CALL;
+ public static final int API_ISINSELFMANAGEDCALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_INCOMING_CALL_PERMITTED;
+ public static final int API_ISOUTGOINGCALLPERMITTED = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_OUTGOING_CALL_PERMITTED;
+ public static final int API_ISRINGING = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_RINGING;
+ public static final int API_ISTTYSUPPORTED = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_TTY_SUPPORTED;
+ public static final int API_ISVOICEMAILNUMBER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_IS_VOICE_MAIL_NUMBER;
+ public static final int API_PLACECALL = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_PLACE_CALL;
+ public static final int API_REGISTERPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_REGISTER_PHONE_ACCOUNT;
+ public static final int API_SETDEFAULTDIALER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_SET_DEFAULT_DIALER;
+ public static final int API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT;
+ public static final int API_SHOWINCALLSCREEN = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_SHOW_IN_CALL_SCREEN;
+ public static final int API_SILENCERINGER = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_SILENCE_RINGER;
+ public static final int API_STARTCONFERENCE = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_START_CONFERENCE;
+ public static final int API_UNREGISTERPHONEACCOUNT = TelecomStatsLog
+ .TELECOM_API_STATS__API_NAME__API_UNREGISTER_PHONE_ACCOUNT;
+ public static final int RESULT_UNKNOWN = TelecomStatsLog
+ .TELECOM_API_STATS__API_RESULT__RESULT_UNKNOWN;
+ public static final int RESULT_NORMAL = TelecomStatsLog
+ .TELECOM_API_STATS__API_RESULT__RESULT_SUCCESS;
+ public static final int RESULT_PERMISSION = TelecomStatsLog
+ .TELECOM_API_STATS__API_RESULT__RESULT_PERMISSION;
+ public static final int RESULT_EXCEPTION = TelecomStatsLog
+ .TELECOM_API_STATS__API_RESULT__RESULT_EXCEPTION;
+ private static final String TAG = ApiStats.class.getSimpleName();
+ private static final String FILE_NAME = "api_stats";
+ private Map<ApiEvent, Integer> mApiStatsMap;
+
+ public ApiStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return TELECOM_API_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.telecomApiStats.length != 0) {
+ Arrays.stream(mPulledAtoms.telecomApiStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getApiName(), v.getUid(), v.getApiResult(), v.getCount())));
+ mApiStatsMap.clear();
+ onAggregate();
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.telecomApiStats != null) {
+ mApiStatsMap = new HashMap<>();
+ for (PulledAtomsClass.TelecomApiStats v : mPulledAtoms.telecomApiStats) {
+ mApiStatsMap.put(new ApiEvent(v.getApiName(), v.getUid(), v.getApiResult()),
+ v.getCount());
+ }
+ mLastPulledTimestamps = mPulledAtoms.getTelecomApiStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ Log.d(TAG, "onAggregate: %s", mApiStatsMap);
+ clearAtoms();
+ if (mApiStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setTelecomApiStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.telecomApiStats =
+ new PulledAtomsClass.TelecomApiStats[mApiStatsMap.size()];
+ int[] index = new int[1];
+ mApiStatsMap.forEach((k, v) -> {
+ mPulledAtoms.telecomApiStats[index[0]] = new PulledAtomsClass.TelecomApiStats();
+ mPulledAtoms.telecomApiStats[index[0]].setApiName(k.mId);
+ mPulledAtoms.telecomApiStats[index[0]].setUid(k.mCallerUid);
+ mPulledAtoms.telecomApiStats[index[0]].setApiResult(k.mResult);
+ mPulledAtoms.telecomApiStats[index[0]].setCount(v);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ public void log(@NonNull ApiEvent event) {
+ post(() -> {
+ mApiStatsMap.put(event, mApiStatsMap.getOrDefault(event, 0) + 1);
+ onAggregate();
+ });
+ }
+
+ @IntDef(prefix = "API", value = {
+ API_UNSPECIFIC,
+ API_ACCEPTHANDOVER,
+ API_ACCEPTRINGINGCALL,
+ API_ACCEPTRINGINGCALLWITHVIDEOSTATE,
+ API_ADDCALL,
+ API_ADDNEWINCOMINGCALL,
+ API_ADDNEWINCOMINGCONFERENCE,
+ API_ADDNEWUNKNOWNCALL,
+ API_CANCELMISSEDCALLSNOTIFICATION,
+ API_CLEARACCOUNTS,
+ API_CREATELAUNCHEMERGENCYDIALERINTENT,
+ API_CREATEMANAGEBLOCKEDNUMBERSINTENT,
+ API_DUMP,
+ API_DUMPCALLANALYTICS,
+ API_ENABLEPHONEACCOUNT,
+ API_ENDCALL,
+ API_GETADNURIFORPHONEACCOUNT,
+ API_GETALLPHONEACCOUNTHANDLES,
+ API_GETALLPHONEACCOUNTS,
+ API_GETALLPHONEACCOUNTSCOUNT,
+ API_GETCALLCAPABLEPHONEACCOUNTS,
+ API_GETCALLSTATE,
+ API_GETCALLSTATEUSINGPACKAGE,
+ API_GETCURRENTTTYMODE,
+ API_GETDEFAULTDIALERPACKAGE,
+ API_GETDEFAULTDIALERPACKAGEFORUSER,
+ API_GETDEFAULTOUTGOINGPHONEACCOUNT,
+ API_GETDEFAULTPHONEAPP,
+ API_GETLINE1NUMBER,
+ API_GETOWNSELFMANAGEDPHONEACCOUNTS,
+ API_GETPHONEACCOUNT,
+ API_GETPHONEACCOUNTSFORPACKAGE,
+ API_GETPHONEACCOUNTSSUPPORTINGSCHEME,
+ API_GETREGISTEREDPHONEACCOUNTS,
+ API_GETSELFMANAGEDPHONEACCOUNTS,
+ API_GETSIMCALLMANAGER,
+ API_GETSIMCALLMANAGERFORUSER,
+ API_GETSYSTEMDIALERPACKAGE,
+ API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+ API_GETVOICEMAILNUMBER,
+ API_HANDLEPINMMI,
+ API_HANDLEPINMMIFORPHONEACCOUNT,
+ API_HASMANAGEONGOINGCALLSPERMISSION,
+ API_ISINCALL,
+ API_ISINCOMINGCALLPERMITTED,
+ API_ISINEMERGENCYCALL,
+ API_ISINMANAGEDCALL,
+ API_ISINSELFMANAGEDCALL,
+ API_ISOUTGOINGCALLPERMITTED,
+ API_ISRINGING,
+ API_ISTTYSUPPORTED,
+ API_ISVOICEMAILNUMBER,
+ API_PLACECALL,
+ API_REGISTERPHONEACCOUNT,
+ API_SETDEFAULTDIALER,
+ API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+ API_SHOWINCALLSCREEN,
+ API_SILENCERINGER,
+ API_STARTCONFERENCE,
+ API_UNREGISTERPHONEACCOUNT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ApiId {
+ }
+
+ @IntDef(prefix = "RESULT", value = {
+ RESULT_UNKNOWN,
+ RESULT_NORMAL,
+ RESULT_PERMISSION,
+ RESULT_EXCEPTION,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResultId {
+ }
+
+ public static class ApiEvent {
+
+ @ApiId
+ int mId;
+ int mCallerUid;
+ @ResultId
+ int mResult;
+
+ public ApiEvent(@ApiId int id, int callerUid, @ResultId int result) {
+ mId = id;
+ mCallerUid = callerUid;
+ mResult = result;
+ }
+
+ public void setCallerUid(int uid) {
+ this.mCallerUid = uid;
+ }
+
+ public void setResult(@ResultId int result) {
+ this.mResult = result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ApiEvent obj)) {
+ return false;
+ }
+ return this.mId == obj.mId && this.mCallerUid == obj.mCallerUid
+ && this.mResult == obj.mResult;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mCallerUid, mResult);
+ }
+
+ @Override
+ public String toString() {
+ return "[ApiEvent: mApiId=" + mId + ", mCallerUid=" + mCallerUid
+ + ", mResult=" + mResult + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/AudioRouteStats.java b/src/com/android/server/telecom/metrics/AudioRouteStats.java
new file mode 100644
index 0000000..4611b22
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/AudioRouteStats.java
@@ -0,0 +1,356 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_LE;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.AudioRoute.TYPE_DOCK;
+import static com.android.server.telecom.AudioRoute.TYPE_EARPIECE;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
+import static com.android.server.telecom.AudioRoute.TYPE_STREAMING;
+import static com.android.server.telecom.AudioRoute.TYPE_WIRED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH_LE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_EARPIECE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_HEARING_AID;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_PHONE_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_UNSPECIFIED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WATCH_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WIRED_HEADSET;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_HEARING_AID;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_PHONE_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_UNSPECIFIED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WATCH_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WIRED_HEADSET;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.telecom.Log;
+import android.util.Pair;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class AudioRouteStats extends TelecomPulledAtom {
+ @VisibleForTesting
+ public static final long THRESHOLD_REVERT_MS = 5000;
+ @VisibleForTesting
+ public static final int EVENT_REVERT_THRESHOLD_EXPIRED = EVENT_SUB_BASE + 1;
+ private static final String TAG = AudioRouteStats.class.getSimpleName();
+ private static final String FILE_NAME = "audio_route_stats";
+ private Map<AudioRouteStatsKey, AudioRouteStatsData> mAudioRouteStatsMap;
+ private Pair<AudioRouteStatsKey, long[]> mCur;
+ private boolean mIsOngoing;
+
+ public AudioRouteStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return CALL_AUDIO_ROUTE_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.callAudioRouteStats.length != 0) {
+ Arrays.stream(mPulledAtoms.callAudioRouteStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getCallAudioRouteSource(), v.getCallAudioRouteDest(),
+ v.getSuccess(), v.getRevert(), v.getCount(), v.getAverageLatencyMs())));
+ mAudioRouteStatsMap.clear();
+ onAggregate();
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.callAudioRouteStats != null) {
+ mAudioRouteStatsMap = new HashMap<>();
+ for (PulledAtomsClass.CallAudioRouteStats v : mPulledAtoms.callAudioRouteStats) {
+ mAudioRouteStatsMap.put(new AudioRouteStatsKey(v.getCallAudioRouteSource(),
+ v.getCallAudioRouteDest(), v.getSuccess(), v.getRevert()),
+ new AudioRouteStatsData(v.getCount(), v.getAverageLatencyMs()));
+ }
+ mLastPulledTimestamps = mPulledAtoms.getCallAudioRouteStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ Log.d(TAG, "onAggregate: %s", mAudioRouteStatsMap);
+ clearAtoms();
+ if (mAudioRouteStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setCallAudioRouteStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.callAudioRouteStats =
+ new PulledAtomsClass.CallAudioRouteStats[mAudioRouteStatsMap.size()];
+ int[] index = new int[1];
+ mAudioRouteStatsMap.forEach((k, v) -> {
+ mPulledAtoms.callAudioRouteStats[index[0]] = new PulledAtomsClass.CallAudioRouteStats();
+ mPulledAtoms.callAudioRouteStats[index[0]].setCallAudioRouteSource(k.mSource);
+ mPulledAtoms.callAudioRouteStats[index[0]].setCallAudioRouteDest(k.mDest);
+ mPulledAtoms.callAudioRouteStats[index[0]].setSuccess(k.mIsSuccess);
+ mPulledAtoms.callAudioRouteStats[index[0]].setRevert(k.mIsRevert);
+ mPulledAtoms.callAudioRouteStats[index[0]].setCount(v.mCount);
+ mPulledAtoms.callAudioRouteStats[index[0]].setAverageLatencyMs(v.mAverageLatency);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ @VisibleForTesting
+ public void log(int source, int target, boolean isSuccess, boolean isRevert, int latency) {
+ post(() -> onLog(new AudioRouteStatsKey(source, target, isSuccess, isRevert), latency));
+ }
+
+ public void onRouteEnter(PendingAudioRoute pendingRoute) {
+ int sourceType = convertAudioType(pendingRoute.getOrigRoute(), true);
+ int destType = convertAudioType(pendingRoute.getDestRoute(), false);
+ long curTime = SystemClock.elapsedRealtime();
+
+ post(() -> {
+ // Ignore the transition route
+ if (!mIsOngoing) {
+ mIsOngoing = true;
+ // Check if the previous route is reverted as the revert time has not been expired.
+ if (mCur != null) {
+ if (destType == mCur.first.getSource() && curTime - mCur.second[0]
+ < THRESHOLD_REVERT_MS) {
+ mCur.first.setRevert(true);
+ }
+ if (mCur.second[1] < 0) {
+ mCur.second[1] = curTime;
+ }
+ onLog();
+ }
+ mCur = new Pair<>(new AudioRouteStatsKey(sourceType, destType), new long[]{curTime,
+ -1});
+ if (hasMessages(EVENT_REVERT_THRESHOLD_EXPIRED)) {
+ // Only keep the latest event
+ removeMessages(EVENT_REVERT_THRESHOLD_EXPIRED);
+ }
+ sendMessageDelayed(
+ obtainMessage(EVENT_REVERT_THRESHOLD_EXPIRED), THRESHOLD_REVERT_MS);
+ }
+ });
+ }
+
+ public void onRouteExit(PendingAudioRoute pendingRoute, boolean isSuccess) {
+ // Check the dest type on the route exiting as it may be different as the enter
+ int destType = convertAudioType(pendingRoute.getDestRoute(), false);
+ long curTime = SystemClock.elapsedRealtime();
+ post(() -> {
+ if (mIsOngoing) {
+ mIsOngoing = false;
+ // Should not be null unless the route is not done before the revert timer expired.
+ if (mCur != null) {
+ mCur.first.setDestType(destType);
+ mCur.first.setSuccess(isSuccess);
+ mCur.second[1] = curTime;
+ }
+ }
+ });
+ }
+
+ private void onLog() {
+ if (mCur != null) {
+ // Ignore the case if the source and dest types are same
+ if (mCur.first.mSource != mCur.first.mDest) {
+ // The route should have been done before the revert timer expires. Otherwise, it
+ // would be logged as the failed case
+ if (mCur.second[1] < 0) {
+ mCur.second[1] = SystemClock.elapsedRealtime();
+ }
+ onLog(mCur.first, (int) (mCur.second[1] - mCur.second[0]));
+ }
+ mCur = null;
+ }
+ }
+
+ private void onLog(AudioRouteStatsKey key, int latency) {
+ AudioRouteStatsData data = mAudioRouteStatsMap.computeIfAbsent(key,
+ k -> new AudioRouteStatsData(0, 0));
+ data.add(latency);
+ onAggregate();
+ }
+
+ private int convertAudioType(AudioRoute route, boolean isSource) {
+ if (route != null) {
+ switch (route.getType()) {
+ case TYPE_EARPIECE:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_EARPIECE;
+ case TYPE_WIRED:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WIRED_HEADSET
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WIRED_HEADSET;
+ case TYPE_SPEAKER:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_PHONE_SPEAKER
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_PHONE_SPEAKER;
+ case TYPE_BLUETOOTH_LE:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH_LE;
+ case TYPE_BLUETOOTH_SCO:
+ if (isSource) {
+ return route.isWatch()
+ ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WATCH_SPEAKER
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH;
+ } else {
+ return route.isWatch()
+ ? CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WATCH_SPEAKER
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH;
+ }
+ case TYPE_BLUETOOTH_HA:
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_HEARING_AID
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_HEARING_AID;
+ case TYPE_DOCK:
+ // Reserved for the future
+ case TYPE_STREAMING:
+ // Reserved for the future
+ default:
+ break;
+ }
+ }
+
+ return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_UNSPECIFIED
+ : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_UNSPECIFIED;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_REVERT_THRESHOLD_EXPIRED:
+ onLog();
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+
+ static class AudioRouteStatsKey {
+
+ final int mSource;
+ int mDest;
+ boolean mIsSuccess;
+ boolean mIsRevert;
+
+ AudioRouteStatsKey(int source, int dest) {
+ mSource = source;
+ mDest = dest;
+ }
+
+ AudioRouteStatsKey(int source, int dest, boolean isSuccess, boolean isRevert) {
+ mSource = source;
+ mDest = dest;
+ mIsSuccess = isSuccess;
+ mIsRevert = isRevert;
+ }
+
+ void setDestType(int dest) {
+ mDest = dest;
+ }
+
+ void setSuccess(boolean isSuccess) {
+ mIsSuccess = isSuccess;
+ }
+
+ void setRevert(boolean isRevert) {
+ mIsRevert = isRevert;
+ }
+
+ int getSource() {
+ return mSource;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AudioRouteStatsKey obj)) {
+ return false;
+ }
+ return this.mSource == obj.mSource && this.mDest == obj.mDest
+ && this.mIsSuccess == obj.mIsSuccess && this.mIsRevert == obj.mIsRevert;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSource, mDest, mIsSuccess, mIsRevert);
+ }
+
+ @Override
+ public String toString() {
+ return "[AudioRouteStatsKey: mSource=" + mSource + ", mDest=" + mDest
+ + ", mIsSuccess=" + mIsSuccess + ", mIsRevert=" + mIsRevert + "]";
+ }
+ }
+
+ static class AudioRouteStatsData {
+
+ int mCount;
+ int mAverageLatency;
+
+ AudioRouteStatsData(int count, int averageLatency) {
+ mCount = count;
+ mAverageLatency = averageLatency;
+ }
+
+ void add(int latency) {
+ mCount++;
+ mAverageLatency += (latency - mAverageLatency) / mCount;
+ }
+
+ @Override
+ public String toString() {
+ return "[AudioRouteStatsData: mCount=" + mCount + ", mAverageLatency:"
+ + mAverageLatency + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/CallStats.java b/src/com/android/server/telecom/metrics/CallStats.java
new file mode 100644
index 0000000..8bdeffb
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/CallStats.java
@@ -0,0 +1,276 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_MANAGED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SELFMANAGED;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_INCOMING;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_OUTGOING;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class CallStats extends TelecomPulledAtom {
+ private static final String TAG = CallStats.class.getSimpleName();
+
+ private static final String FILE_NAME = "call_stats";
+ private final Set<String> mOngoingCallsWithoutMultipleAudioDevices = new HashSet<>();
+ private final Set<String> mOngoingCallsWithMultipleAudioDevices = new HashSet<>();
+ private Map<CallStatsKey, CallStatsData> mCallStatsMap;
+ private boolean mHasMultipleAudioDevices;
+
+ public CallStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return CALL_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.callStats.length != 0) {
+ Arrays.stream(mPulledAtoms.callStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getCallDirection(), v.getExternalCall(), v.getEmergencyCall(),
+ v.getMultipleAudioAvailable(), v.getAccountType(), v.getUid(),
+ v.getCount(), v.getAverageDurationMs())));
+ mCallStatsMap.clear();
+ onAggregate();
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.callStats != null) {
+ mCallStatsMap = new HashMap<>();
+ for (PulledAtomsClass.CallStats v : mPulledAtoms.callStats) {
+ mCallStatsMap.put(new CallStatsKey(v.getCallDirection(),
+ v.getExternalCall(), v.getEmergencyCall(),
+ v.getMultipleAudioAvailable(),
+ v.getAccountType(), v.getUid()),
+ new CallStatsData(v.getCount(), v.getAverageDurationMs()));
+ }
+ mLastPulledTimestamps = mPulledAtoms.getCallStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ Log.d(TAG, "onAggregate: %s", mCallStatsMap);
+ clearAtoms();
+ if (mCallStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setCallStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.callStats = new PulledAtomsClass.CallStats[mCallStatsMap.size()];
+ int[] index = new int[1];
+ mCallStatsMap.forEach((k, v) -> {
+ mPulledAtoms.callStats[index[0]] = new PulledAtomsClass.CallStats();
+ mPulledAtoms.callStats[index[0]].setCallDirection(k.mDirection);
+ mPulledAtoms.callStats[index[0]].setExternalCall(k.mIsExternal);
+ mPulledAtoms.callStats[index[0]].setEmergencyCall(k.mIsEmergency);
+ mPulledAtoms.callStats[index[0]].setMultipleAudioAvailable(k.mIsMultipleAudioAvailable);
+ mPulledAtoms.callStats[index[0]].setAccountType(k.mAccountType);
+ mPulledAtoms.callStats[index[0]].setUid(k.mUid);
+ mPulledAtoms.callStats[index[0]].setCount(v.mCount);
+ mPulledAtoms.callStats[index[0]].setAverageDurationMs(v.mAverageDuration);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ public void log(int direction, boolean isExternal, boolean isEmergency,
+ boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
+ post(() -> {
+ CallStatsKey key = new CallStatsKey(direction, isExternal, isEmergency,
+ isMultipleAudioAvailable, accountType, uid);
+ CallStatsData data = mCallStatsMap.computeIfAbsent(key, k -> new CallStatsData(0, 0));
+ data.add(duration);
+ onAggregate();
+ });
+ }
+
+ public void onCallStart(Call call) {
+ post(() -> {
+ if (mHasMultipleAudioDevices) {
+ mOngoingCallsWithMultipleAudioDevices.add(call.getId());
+ } else {
+ mOngoingCallsWithoutMultipleAudioDevices.add(call.getId());
+ }
+ });
+ }
+
+ public void onCallEnd(Call call) {
+ final int duration = (int) (call.getAgeMillis());
+ post(() -> {
+ final boolean hasMultipleAudioDevices = mOngoingCallsWithMultipleAudioDevices.remove(
+ call.getId());
+ final int direction = call.isIncoming() ? CALL_STATS__CALL_DIRECTION__DIR_INCOMING
+ : (call.isOutgoing() ? CALL_STATS__CALL_DIRECTION__DIR_OUTGOING
+ : CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN);
+ final int accountType = getAccountType(call.getPhoneAccountFromHandle());
+ int uid = call.getCallingPackageIdentity().mCallingPackageUid;
+ try {
+ uid = mContext.getPackageManager().getApplicationInfo(
+ call.getTargetPhoneAccount().getComponentName().getPackageName(), 0).uid;
+ } catch (Exception e) {
+ Log.i(TAG, "failed to get the uid for " + e);
+ }
+
+ log(direction, call.isExternalCall(), call.isEmergencyCall(), hasMultipleAudioDevices,
+ accountType, uid, duration);
+ });
+ }
+
+ private int getAccountType(PhoneAccount account) {
+ if (account == null) {
+ return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
+ }
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+ return account.hasCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ ? CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API
+ : CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SELFMANAGED;
+ }
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
+ return account.hasCapabilities(
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ ? CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM
+ : CALL_STATS__ACCOUNT_TYPE__ACCOUNT_MANAGED;
+ }
+ return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
+ }
+
+ public void onAudioDevicesChange(boolean hasMultipleAudioDevices) {
+ post(() -> {
+ if (mHasMultipleAudioDevices != hasMultipleAudioDevices) {
+ mHasMultipleAudioDevices = hasMultipleAudioDevices;
+ if (mHasMultipleAudioDevices) {
+ mOngoingCallsWithMultipleAudioDevices.addAll(
+ mOngoingCallsWithoutMultipleAudioDevices);
+ mOngoingCallsWithoutMultipleAudioDevices.clear();
+ }
+ }
+ });
+ }
+
+ static class CallStatsKey {
+ final int mDirection;
+ final boolean mIsExternal;
+ final boolean mIsEmergency;
+ final boolean mIsMultipleAudioAvailable;
+ final int mAccountType;
+ final int mUid;
+
+ CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
+ boolean isMultipleAudioAvailable, int accountType, int uid) {
+ mDirection = direction;
+ mIsExternal = isExternal;
+ mIsEmergency = isEmergency;
+ mIsMultipleAudioAvailable = isMultipleAudioAvailable;
+ mAccountType = accountType;
+ mUid = uid;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CallStatsKey obj)) {
+ return false;
+ }
+ return this.mDirection == obj.mDirection && this.mIsExternal == obj.mIsExternal
+ && this.mIsEmergency == obj.mIsEmergency
+ && this.mIsMultipleAudioAvailable == obj.mIsMultipleAudioAvailable
+ && this.mAccountType == obj.mAccountType && this.mUid == obj.mUid;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDirection, mIsExternal, mIsEmergency, mIsMultipleAudioAvailable,
+ mAccountType, mUid);
+ }
+
+ @Override
+ public String toString() {
+ return "[CallStatsKey: mDirection=" + mDirection + ", mIsExternal=" + mIsExternal
+ + ", mIsEmergency=" + mIsEmergency + ", mIsMultipleAudioAvailable="
+ + mIsMultipleAudioAvailable + ", mAccountType=" + mAccountType + ", mUid="
+ + mUid + "]";
+ }
+ }
+
+ static class CallStatsData {
+
+ int mCount;
+ int mAverageDuration;
+
+ CallStatsData(int count, int averageDuration) {
+ mCount = count;
+ mAverageDuration = averageDuration;
+ }
+
+ void add(int duration) {
+ mCount++;
+ mAverageDuration += (duration - mAverageDuration) / mCount;
+ }
+
+ @Override
+ public String toString() {
+ return "[CallStatsData: mCount=" + mCount + ", mAverageDuration:" + mAverageDuration
+ + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/ErrorStats.java b/src/com/android/server/telecom/metrics/ErrorStats.java
new file mode 100644
index 0000000..f334710
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/ErrorStats.java
@@ -0,0 +1,274 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class ErrorStats extends TelecomPulledAtom {
+ public static final int SUB_UNKNOWN = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_UNKNOWN;
+ public static final int SUB_CALL_AUDIO = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_CALL_AUDIO;
+ public static final int SUB_CALL_LOGS = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_CALL_LOGS;
+ public static final int SUB_CALL_MANAGER = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_CALL_MANAGER;
+ public static final int SUB_CONNECTION_SERVICE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_CONNECTION_SERVICE;
+ public static final int SUB_EMERGENCY_CALL = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_EMERGENCY_CALL;
+ public static final int SUB_IN_CALL_SERVICE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_IN_CALL_SERVICE;
+ public static final int SUB_MISC = TelecomStatsLog.TELECOM_ERROR_STATS__SUBMODULE__SUB_MISC;
+ public static final int SUB_PHONE_ACCOUNT = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_PHONE_ACCOUNT;
+ public static final int SUB_SYSTEM_SERVICE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_SYSTEM_SERVICE;
+ public static final int SUB_TELEPHONY = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_TELEPHONY;
+ public static final int SUB_UI = TelecomStatsLog.TELECOM_ERROR_STATS__SUBMODULE__SUB_UI;
+ public static final int SUB_VOIP_CALL = TelecomStatsLog
+ .TELECOM_ERROR_STATS__SUBMODULE__SUB_VOIP_CALL;
+ public static final int ERROR_UNKNOWN = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_UNKNOWN;
+ public static final int ERROR_EXTERNAL_EXCEPTION = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_EXTERNAL_EXCEPTION;
+ public static final int ERROR_INTERNAL_EXCEPTION = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_INTERNAL_EXCEPTION;
+ public static final int ERROR_AUDIO_ROUTE_RETRY_REJECTED = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_AUDIO_ROUTE_RETRY_REJECTED;
+ public static final int ERROR_BT_GET_SERVICE_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_BT_GET_SERVICE_FAILURE;
+ public static final int ERROR_BT_REGISTER_CALLBACK_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_BT_REGISTER_CALLBACK_FAILURE;
+ public static final int ERROR_AUDIO_ROUTE_UNAVAILABLE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_AUDIO_ROUTE_UNAVAILABLE;
+ public static final int ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE;
+ public static final int ERROR_NOTIFY_CALL_STREAM_START_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_NOTIFY_CALL_STREAM_START_FAILURE;
+ public static final int ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE;
+ public static final int ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE;
+ public static final int ERROR_RTT_STREAM_CLOSE_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_RTT_STREAM_CLOSE_FAILURE;
+ public static final int ERROR_RTT_STREAM_CREATE_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_RTT_STREAM_CREATE_FAILURE;
+ public static final int ERROR_SET_MUTED_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_SET_MUTED_FAILURE;
+ public static final int ERROR_VIDEO_PROVIDER_SET_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_VIDEO_PROVIDER_SET_FAILURE;
+ public static final int ERROR_WIRED_HEADSET_NOT_AVAILABLE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_WIRED_HEADSET_NOT_AVAILABLE;
+ public static final int ERROR_LOG_CALL_FAILURE = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_LOG_CALL_FAILURE;
+ public static final int ERROR_RETRIEVING_ACCOUNT_EMERGENCY = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_RETRIEVING_ACCOUNT_EMERGENCY;
+ public static final int ERROR_RETRIEVING_ACCOUNT = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_RETRIEVING_ACCOUNT;
+ public static final int ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT;
+ public static final int ERROR_DEFAULT_MO_ACCOUNT_MISMATCH = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_DEFAULT_MO_ACCOUNT_MISMATCH;
+ public static final int ERROR_ESTABLISHING_CONNECTION = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_ESTABLISHING_CONNECTION;
+ public static final int ERROR_REMOVING_CALL = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_REMOVING_CALL;
+ public static final int ERROR_STUCK_CONNECTING_EMERGENCY = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_STUCK_CONNECTING_EMERGENCY;
+ public static final int ERROR_STUCK_CONNECTING = TelecomStatsLog
+ .TELECOM_ERROR_STATS__ERROR__ERROR_STUCK_CONNECTING;
+ private static final String TAG = ErrorStats.class.getSimpleName();
+ private static final String FILE_NAME = "error_stats";
+ private Map<ErrorEvent, Integer> mErrorStatsMap;
+
+ public ErrorStats(@NonNull Context context, @NonNull Looper looper) {
+ super(context, looper);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public int getTag() {
+ return TELECOM_ERROR_STATS;
+ }
+
+ @Override
+ protected String getFileName() {
+ return FILE_NAME;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized int onPull(final List<StatsEvent> data) {
+ if (mPulledAtoms.telecomErrorStats.length != 0) {
+ Arrays.stream(mPulledAtoms.telecomErrorStats).forEach(v -> data.add(
+ TelecomStatsLog.buildStatsEvent(getTag(),
+ v.getSubmodule(), v.getError(), v.getCount())));
+ mErrorStatsMap.clear();
+ onAggregate();
+ return StatsManager.PULL_SUCCESS;
+ } else {
+ return StatsManager.PULL_SKIP;
+ }
+ }
+
+ @Override
+ protected synchronized void onLoad() {
+ if (mPulledAtoms.telecomErrorStats != null) {
+ mErrorStatsMap = new HashMap<>();
+ for (PulledAtomsClass.TelecomErrorStats v : mPulledAtoms.telecomErrorStats) {
+ mErrorStatsMap.put(new ErrorEvent(v.getSubmodule(), v.getError()),
+ v.getCount());
+ }
+ mLastPulledTimestamps = mPulledAtoms.getTelecomErrorStatsPullTimestampMillis();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ @Override
+ public synchronized void onAggregate() {
+ Log.d(TAG, "onAggregate: %s", mErrorStatsMap);
+ clearAtoms();
+ if (mErrorStatsMap.isEmpty()) {
+ return;
+ }
+ mPulledAtoms.setTelecomErrorStatsPullTimestampMillis(mLastPulledTimestamps);
+ mPulledAtoms.telecomErrorStats =
+ new PulledAtomsClass.TelecomErrorStats[mErrorStatsMap.size()];
+ int[] index = new int[1];
+ mErrorStatsMap.forEach((k, v) -> {
+ mPulledAtoms.telecomErrorStats[index[0]] = new PulledAtomsClass.TelecomErrorStats();
+ mPulledAtoms.telecomErrorStats[index[0]].setSubmodule(k.mModuleId);
+ mPulledAtoms.telecomErrorStats[index[0]].setError(k.mErrorId);
+ mPulledAtoms.telecomErrorStats[index[0]].setCount(v);
+ index[0]++;
+ });
+ save(DELAY_FOR_PERSISTENT_MILLIS);
+ }
+
+ public void log(@SubModuleId int moduleId, @ErrorId int errorId) {
+ post(() -> {
+ ErrorEvent key = new ErrorEvent(moduleId, errorId);
+ mErrorStatsMap.put(key, mErrorStatsMap.getOrDefault(key, 0) + 1);
+ onAggregate();
+ });
+ }
+
+ @IntDef(prefix = "SUB", value = {
+ SUB_UNKNOWN,
+ SUB_CALL_AUDIO,
+ SUB_CALL_LOGS,
+ SUB_CALL_MANAGER,
+ SUB_CONNECTION_SERVICE,
+ SUB_EMERGENCY_CALL,
+ SUB_IN_CALL_SERVICE,
+ SUB_MISC,
+ SUB_PHONE_ACCOUNT,
+ SUB_SYSTEM_SERVICE,
+ SUB_TELEPHONY,
+ SUB_UI,
+ SUB_VOIP_CALL,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SubModuleId {
+ }
+
+ @IntDef(prefix = "ERROR", value = {
+ ERROR_UNKNOWN,
+ ERROR_EXTERNAL_EXCEPTION,
+ ERROR_INTERNAL_EXCEPTION,
+ ERROR_AUDIO_ROUTE_RETRY_REJECTED,
+ ERROR_BT_GET_SERVICE_FAILURE,
+ ERROR_BT_REGISTER_CALLBACK_FAILURE,
+ ERROR_AUDIO_ROUTE_UNAVAILABLE,
+ ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE,
+ ERROR_NOTIFY_CALL_STREAM_START_FAILURE,
+ ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE,
+ ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE,
+ ERROR_RTT_STREAM_CLOSE_FAILURE,
+ ERROR_RTT_STREAM_CREATE_FAILURE,
+ ERROR_SET_MUTED_FAILURE,
+ ERROR_VIDEO_PROVIDER_SET_FAILURE,
+ ERROR_WIRED_HEADSET_NOT_AVAILABLE,
+ ERROR_LOG_CALL_FAILURE,
+ ERROR_RETRIEVING_ACCOUNT_EMERGENCY,
+ ERROR_RETRIEVING_ACCOUNT,
+ ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT,
+ ERROR_DEFAULT_MO_ACCOUNT_MISMATCH,
+ ERROR_ESTABLISHING_CONNECTION,
+ ERROR_REMOVING_CALL,
+ ERROR_STUCK_CONNECTING_EMERGENCY,
+ ERROR_STUCK_CONNECTING,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ErrorId {
+ }
+
+ static class ErrorEvent {
+
+ final @SubModuleId int mModuleId;
+ final @ErrorId int mErrorId;
+
+ ErrorEvent(@SubModuleId int moduleId, @ErrorId int errorId) {
+ mModuleId = moduleId;
+ mErrorId = errorId;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ErrorEvent obj)) {
+ return false;
+ }
+ return this.mModuleId == obj.mModuleId && this.mErrorId == obj.mErrorId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mModuleId, mErrorId);
+ }
+
+ @Override
+ public String toString() {
+ return "[ErrorEvent: mModuleId=" + mModuleId + ", mErrorId=" + mErrorId + "]";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/TelecomMetricsController.java b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
new file mode 100644
index 0000000..df735c0
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
@@ -0,0 +1,147 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.modules.utils.HandlerExecutor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class TelecomMetricsController implements StatsManager.StatsPullAtomCallback {
+
+ private static final String TAG = TelecomMetricsController.class.getSimpleName();
+
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final ConcurrentHashMap<Integer, TelecomPulledAtom> mStats = new ConcurrentHashMap<>();
+
+ private TelecomMetricsController(@NonNull Context context,
+ @NonNull HandlerThread handlerThread) {
+ mContext = context;
+ mHandlerThread = handlerThread;
+ }
+
+ @NonNull
+ public static TelecomMetricsController make(@NonNull Context context) {
+ Log.i(TAG, "TMC.m1");
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ return make(context, handlerThread);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public static TelecomMetricsController make(@NonNull Context context,
+ @NonNull HandlerThread handlerThread) {
+ Log.i(TAG, "TMC.m2");
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(handlerThread);
+ return new TelecomMetricsController(context, handlerThread);
+ }
+
+ @NonNull
+ public ApiStats getApiStats() {
+ ApiStats stats = (ApiStats) mStats.get(TELECOM_API_STATS);
+ if (stats == null) {
+ stats = new ApiStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @NonNull
+ public AudioRouteStats getAudioRouteStats() {
+ AudioRouteStats stats = (AudioRouteStats) mStats.get(CALL_AUDIO_ROUTE_STATS);
+ if (stats == null) {
+ stats = new AudioRouteStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @NonNull
+ public CallStats getCallStats() {
+ CallStats stats = (CallStats) mStats.get(CALL_STATS);
+ if (stats == null) {
+ stats = new CallStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @NonNull
+ public ErrorStats getErrorStats() {
+ ErrorStats stats = (ErrorStats) mStats.get(TELECOM_ERROR_STATS);
+ if (stats == null) {
+ stats = new ErrorStats(mContext, mHandlerThread.getLooper());
+ registerAtom(stats.getTag(), stats);
+ }
+ return stats;
+ }
+
+ @Override
+ public int onPullAtom(final int atomTag, final List<StatsEvent> data) {
+ if (mStats.containsKey(atomTag)) {
+ return Objects.requireNonNull(mStats.get(atomTag)).pull(data);
+ }
+ return StatsManager.PULL_SKIP;
+ }
+
+ @VisibleForTesting
+ public Map<Integer, TelecomPulledAtom> getStats() {
+ return mStats;
+ }
+
+ @VisibleForTesting
+ public void registerAtom(int tag, TelecomPulledAtom atom) {
+ final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+ if (statsManager != null) {
+ statsManager.setPullAtomCallback(tag, null, new HandlerExecutor(atom), this);
+ mStats.put(tag, atom);
+ } else {
+ Log.w(TAG, "Unable to register the pulled atom as StatsManager is null");
+ }
+ }
+
+ public void destroy() {
+ final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+ if (statsManager != null) {
+ mStats.forEach((tag, stat) -> statsManager.clearPullAtomCallback(tag));
+ } else {
+ Log.w(TAG, "Unable to clear pulled atoms as StatsManager is null");
+ }
+
+ mStats.clear();
+ mHandlerThread.quitSafely();
+ }
+}
diff --git a/src/com/android/server/telecom/metrics/TelecomPulledAtom.java b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
new file mode 100644
index 0000000..161eaa8
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
@@ -0,0 +1,135 @@
+/*
+ * 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.metrics;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.nano.PulledAtomsClass.PulledAtoms;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.util.List;
+
+public abstract class TelecomPulledAtom extends Handler {
+ /**
+ * Min interval to persist the data.
+ */
+ protected static final int DELAY_FOR_PERSISTENT_MILLIS = 30000;
+ protected static final int EVENT_SUB_BASE = 1000;
+ private static final String TAG = TelecomPulledAtom.class.getSimpleName();
+ private static final long MIN_PULL_INTERVAL_MILLIS = 23L * 60 * 60 * 1000;
+ private static final int EVENT_SAVE = 1;
+ protected final Context mContext;
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public PulledAtoms mPulledAtoms;
+ protected long mLastPulledTimestamps;
+
+ protected TelecomPulledAtom(@NonNull Context context, @NonNull Looper looper) {
+ super(looper);
+ mContext = context;
+ mPulledAtoms = loadAtomsFromFile();
+ onLoad();
+ }
+
+ public synchronized int pull(final List<StatsEvent> data) {
+ long cur = System.currentTimeMillis();
+ if (cur - mLastPulledTimestamps < MIN_PULL_INTERVAL_MILLIS) {
+ return StatsManager.PULL_SKIP;
+ }
+ mLastPulledTimestamps = cur;
+ return onPull(data);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ abstract public int getTag();
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public abstract int onPull(List<StatsEvent> data);
+
+ protected abstract void onLoad();
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public abstract void onAggregate();
+
+ public void onFlush() {
+ save(0);
+ }
+
+ protected abstract String getFileName();
+
+ private synchronized PulledAtoms loadAtomsFromFile() {
+ try {
+ return
+ PulledAtoms.parseFrom(
+ Files.readAllBytes(mContext.getFileStreamPath(getFileName()).toPath()));
+ } catch (NoSuchFileException e) {
+ Log.e(TAG, e, "the atom file not found");
+ } catch (IOException | NullPointerException e) {
+ Log.e(TAG, e, "cannot load/parse the atom file");
+ }
+ return makeNewPulledAtoms();
+ }
+
+ protected synchronized void clearAtoms() {
+ mPulledAtoms = makeNewPulledAtoms();
+ }
+
+ private synchronized void onSave() {
+ try (FileOutputStream stream = mContext.openFileOutput(getFileName(),
+ Context.MODE_PRIVATE)) {
+ Log.d(TAG, "save " + getTag());
+ stream.write(PulledAtoms.toByteArray(mPulledAtoms));
+ } catch (IOException e) {
+ Log.e(TAG, e, "cannot save the atom to file");
+ } catch (UnsupportedOperationException e) {
+ Log.e(TAG, e, "cannot open the file");
+ }
+ }
+
+ private PulledAtoms makeNewPulledAtoms() {
+ return new PulledAtoms();
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public void save(int delayMillis) {
+ if (delayMillis > 0) {
+ if (!hasMessages(EVENT_SAVE)) {
+ sendMessageDelayed(obtainMessage(EVENT_SAVE), delayMillis);
+ }
+ } else {
+ onSave();
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_SAVE) {
+ onSave();
+ }
+ }
+}
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 5fa5f06..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.SystemContract.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,7 +193,10 @@
}
private void updateButterBar() {
- if (BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed) {
+ boolean isBlockSuppressionEnabled = mBlockedNumbersManager != null
+ ? mBlockedNumbersManager.getBlockSuppressionStatus().getIsSuppressed()
+ : BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed;
+ if (isBlockSuppressionEnabled) {
mButterBar.setVisibility(View.VISIBLE);
} else {
mButterBar.setVisibility(View.GONE);
@@ -238,7 +251,11 @@
if (view == mAddButton) {
showAddBlockedNumberDialog();
} else if (view == mReEnableButton) {
- BlockedNumberContract.SystemContract.endBlockSuppression(this);
+ if (mBlockedNumbersManager != null) {
+ mBlockedNumbersManager.endBlockSuppression();
+ } else {
+ BlockedNumberContract.SystemContract.endBlockSuppression(this);
+ }
mButterBar.setVisibility(View.GONE);
}
}
@@ -301,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 4be75f8..99c5746 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.SystemContract;
+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;
@@ -131,9 +133,12 @@
public static boolean isEnhancedCallBlockingEnabledByPlatform(Context context) {
CarrierConfigManager configManager = (CarrierConfigManager) context.getSystemService(
Context.CARRIER_CONFIG_SERVICE);
- PersistableBundle carrierConfig = configManager.getConfig();
+ PersistableBundle carrierConfig = null;
+ if (configManager != null) {
+ carrierConfig = configManager.getConfig();
+ }
if (carrierConfig == null) {
- carrierConfig = configManager.getDefaultConfig();
+ carrierConfig = CarrierConfigManager.getDefaultConfig();
}
return carrierConfig.getBoolean(
CarrierConfigManager.KEY_SUPPORT_ENHANCED_CALL_BLOCKING_BOOL)
@@ -148,8 +153,11 @@
* @return If {@code true} means the key enabled in the SharedPreferences,
* {@code false} otherwise.
*/
- public static boolean getEnhancedBlockSetting(Context context, String key) {
- return SystemContract.getEnhancedBlockSetting(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 +167,13 @@
* @param key preference key of SharedPreferences.
* @param value the register value to the SharedPreferences.
*/
- public static void setEnhancedBlockSetting(Context context, String key, boolean value) {
- SystemContract.setEnhancedBlockSetting(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 5f42b37..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();
}
@@ -58,11 +62,11 @@
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
- BlockedNumbersUtil.setEnhancedBlockSetting(
+ BlockedNumbersUtil.setBlockedNumberSetting(
CallBlockDisabledActivity.this,
- BlockedNumberContract.SystemContract
+ 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/EnableAccountPreferenceFragment.java b/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java
index c2a0500..d9feaff 100644
--- a/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java
+++ b/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java
@@ -72,7 +72,8 @@
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mTelecomManager = TelecomManager.from(getActivity());
+ Context context = getActivity();
+ mTelecomManager = context.getSystemService(TelecomManager.class);
}
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index b1a1b0e..b54e273 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,16 +23,17 @@
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumbersManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telecom.Log;
import android.view.LayoutInflater;
-import android.view.MenuItem;
import android.view.View;
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 {
@@ -46,21 +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(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
- setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
- setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
- setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
- setOnPreferenceChangeListener(SystemContract.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(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+ Preference payPhoneOption = getPreferenceScreen()
+ .findPreference(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
getPreferenceScreen().removePreference(payPhoneOption);
}
}
@@ -122,13 +127,13 @@
public void onResume() {
super.onResume();
- updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
- updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
if (showPayPhoneBlocking()) {
- updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+ updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
}
- updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
- updateEnhancedBlockPref(SystemContract.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.getEnhancedBlockSetting(getActivity(), key));
+ pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(
+ getActivity(), key, mFeatureFlags));
}
}
@@ -147,19 +153,19 @@
if (mIsCombiningRestrictedAndUnknownOption) {
Log.i(this, "onPreferenceChange: changing %s and %s to %b",
preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
- BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
- BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+ BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
+ 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.setEnhancedBlockSetting(getActivity(),
- BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+ BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
+ BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
}
}
- BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(), preference.getKey(),
- (boolean) objValue);
+ BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
+ (boolean) objValue, mFeatureFlags);
return true;
}
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
index 8414047..06da5e3 100644
--- a/src/com/android/server/telecom/ui/CallStreamingNotification.java
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -192,7 +192,7 @@
// Use the caller name for the label if available, default to app name if none.
if (TextUtils.isEmpty(callerName)) {
// App did not provide a caller name, so default to app's name.
- callerName = mAppLabelProxy.getAppLabel(appPackageName).toString();
+ callerName = mAppLabelProxy.getAppLabel(appPackageName, userHandle).toString();
}
// Action to hangup; this can use the default hangup action from the call style
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 6b97f97..220b44e 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -20,6 +20,7 @@
import static android.app.admin.DevicePolicyResources.Strings.Telecomm.NOTIFICATION_MISSED_WORK_CALL_TITLE;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
@@ -42,6 +43,7 @@
import android.os.Binder;
import android.os.Bundle;
import android.os.UserHandle;
+import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.telecom.CallerInfo;
import android.telecom.Log;
@@ -57,11 +59,13 @@
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;
import com.android.server.telecom.DefaultDialerCache;
import com.android.server.telecom.DeviceIdleControllerAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.MissedCallNotifier;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.R;
@@ -87,7 +91,8 @@
MissedCallNotifier makeMissedCallNotifierImpl(Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
- DeviceIdleControllerAdapter deviceIdleControllerAdapter);
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ FeatureFlags featureFlags);
}
public interface NotificationBuilderFactory {
@@ -141,19 +146,22 @@
private final Map<UserHandle, Integer> mMissedCallCounts;
private Set<UserHandle> mUsersToLoadAfterBootComplete = new ArraySet<>();
+ private FeatureFlags mFeatureFlags;
public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
- DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ FeatureFlags featureFlags) {
this(context, phoneAccountRegistrar, defaultDialerCache,
- new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter);
+ new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter, featureFlags);
}
public MissedCallNotifierImpl(Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
NotificationBuilderFactory notificationBuilderFactory,
- DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ FeatureFlags featureFlags) {
mContext = context;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mNotificationManager =
@@ -163,6 +171,7 @@
mNotificationBuilderFactory = notificationBuilderFactory;
mMissedCallCounts = new ArrayMap<>();
+ mFeatureFlags = featureFlags;
}
/** Clears missed call notification and marks the call log's missed calls as read. */
@@ -261,17 +270,17 @@
}
private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo,
- UserHandle userHandle, int missedCallCount) {
+ UserHandle userHandle, int missedCallCount, @Nullable Uri uri) {
Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage)
.setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT,
createClearMissedCallsPendingIntent(userHandle))
.putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, missedCallCount)
+ .putExtra(TelecomManager.EXTRA_CALL_LOG_URI, uri)
.putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER,
callInfo == null ? null : callInfo.getPhoneNumber())
.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
callInfo == null ? null : callInfo.getPhoneAccountHandle());
-
if (missedCallCount == 1 && callInfo != null) {
final Uri handleUri = callInfo.getHandle();
String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
@@ -295,7 +304,7 @@
* @param callInfo The missed call.
*/
@Override
- public void showMissedCallNotification(@NonNull CallInfo callInfo) {
+ public void showMissedCallNotification(@NonNull CallInfo callInfo, @Nullable Uri uri) {
final PhoneAccountHandle phoneAccountHandle = callInfo.getPhoneAccountHandle();
final PhoneAccount phoneAccount =
mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
@@ -306,10 +315,11 @@
} else {
userHandle = phoneAccountHandle.getUserHandle();
}
- showMissedCallNotification(callInfo, userHandle);
+ showMissedCallNotification(callInfo, userHandle, uri);
}
- private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle) {
+ private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle,
+ @Nullable Uri uri) {
int missedCallCounts;
synchronized (mMissedCallCountsLock) {
Integer currentCount = mMissedCallCounts.get(userHandle);
@@ -324,7 +334,7 @@
String dialerPackage = getDefaultDialerPackage(userHandle);
if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle,
- missedCallCounts);
+ missedCallCounts, uri);
return;
}
@@ -446,7 +456,7 @@
String dialerPackage = getDefaultDialerPackage(userHandle);
if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle,
- 0 /* missedCallCount */);
+ /* missedCallCount= */ 0, /* uri= */ null);
return;
}
@@ -497,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();
@@ -631,6 +647,13 @@
while (cursor.moveToNext()) {
// Get data about the missed call from the cursor
final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER);
+ final Uri uri;
+ if (mFeatureFlags.addCallUriForMissedCalls()){
+ uri = Calls.CONTENT_URI.buildUpon().appendPath(
+ Long.toString(cursor.getInt(CALL_LOG_COLUMN_ID))).build();
+ }else{
+ uri = null;
+ }
final int presentation =
cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION);
final long date = cursor.getLong(CALL_LOG_COLUMN_DATE);
@@ -663,7 +686,8 @@
// null, just show the notification.
CallInfo callInfo = callInfoFactory.makeCallInfo(
info, null, handle, date);
- showMissedCallNotification(callInfo, userHandle);
+ showMissedCallNotification(callInfo, userHandle,
+ /* uri= */ uri);
}
}
@@ -678,7 +702,8 @@
}
CallInfo callInfo = callInfoFactory.makeCallInfo(
info, null, handle, date);
- showMissedCallNotification(callInfo, userHandle);
+ showMissedCallNotification(callInfo, userHandle,
+ /* uri= */ uri);
}
}
);
diff --git a/src/com/android/server/telecom/ui/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index b3cb2c3..987b6b3 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -83,61 +83,62 @@
boolean vibration = false;
Uri sound = silentRingtone;
switch (channelId) {
- case CHANNEL_ID_INCOMING_CALLS:
+ case CHANNEL_ID_INCOMING_CALLS -> {
name = context.getText(R.string.notification_channel_incoming_call);
importance = NotificationManager.IMPORTANCE_MAX;
canShowBadge = false;
lights = true;
vibration = false;
sound = silentRingtone;
- break;
- case CHANNEL_ID_MISSED_CALLS:
+ }
+ case CHANNEL_ID_MISSED_CALLS -> {
name = context.getText(R.string.notification_channel_missed_call);
importance = NotificationManager.IMPORTANCE_DEFAULT;
canShowBadge = true;
lights = true;
vibration = true;
sound = silentRingtone;
- break;
- case CHANNEL_ID_CALL_BLOCKING:
+ }
+ case CHANNEL_ID_CALL_BLOCKING -> {
name = context.getText(R.string.notification_channel_call_blocking);
importance = NotificationManager.IMPORTANCE_LOW;
canShowBadge = false;
lights = false;
vibration = false;
sound = null;
- break;
- case CHANNEL_ID_AUDIO_PROCESSING:
+ }
+ case CHANNEL_ID_AUDIO_PROCESSING -> {
name = context.getText(R.string.notification_channel_background_calls);
importance = NotificationManager.IMPORTANCE_LOW;
canShowBadge = false;
lights = false;
vibration = false;
sound = null;
- break;
- case CHANNEL_ID_DISCONNECTED_CALLS:
+ }
+ case CHANNEL_ID_DISCONNECTED_CALLS -> {
name = context.getText(R.string.notification_channel_disconnected_calls);
importance = NotificationManager.IMPORTANCE_DEFAULT;
canShowBadge = true;
lights = true;
vibration = true;
sound = silentRingtone;
- break;
- case CHANNEL_ID_IN_CALL_SERVICE_CRASH:
+ }
+ case CHANNEL_ID_IN_CALL_SERVICE_CRASH -> {
name = context.getText(R.string.notification_channel_in_call_service_crash);
importance = NotificationManager.IMPORTANCE_DEFAULT;
canShowBadge = true;
lights = true;
vibration = true;
sound = null;
- case CHANNEL_ID_CALL_STREAMING:
+ }
+ case CHANNEL_ID_CALL_STREAMING -> {
name = context.getText(R.string.notification_channel_call_streaming);
importance = NotificationManager.IMPORTANCE_DEFAULT;
canShowBadge = false;
lights = false;
vibration = false;
sound = null;
- break;
+ }
}
NotificationChannel channel = new NotificationChannel(channelId, name, importance);
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/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
deleted file mode 100644
index 6176087..0000000
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import com.android.server.telecom.LoggedHandlerExecutor;
-import com.android.server.telecom.TelecomSystem;
-
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * A VoipCallTransaction implementation that its sub transactions will be executed in parallel
- */
-public class ParallelTransaction extends VoipCallTransaction {
- public ParallelTransaction(List<VoipCallTransaction> subTransactions,
- TelecomSystem.SyncRoot lock) {
- super(subTransactions, lock);
- }
-
- @Override
- public void start() {
- // 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);
- }
- finish();
- return null;
- }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
- + ".s", mLock));
-
- 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();
- return null;
- }, new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode()
- + ".oTC", mLock));
- } else {
- if (mCount.decrementAndGet() == 0) {
- scheduleTransaction();
- }
- }
- }
-
- @Override
- public void onTransactionTimeout(String transactionName) {
- CompletableFuture.completedFuture(null).thenApplyAsync(
- (x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s timed out",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish();
- return null;
- }, new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode()
- + ".oTT", mLock));
- }
- };
- for (VoipCallTransaction transaction : mSubTransactions) {
- transaction.setCompleteListener(subTransactionListener);
- transaction.start();
- }
- } else {
- scheduleTransaction();
- }
- }
-}
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
deleted file mode 100644
index b35b471..0000000
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import com.android.server.telecom.LoggedHandlerExecutor;
-import com.android.server.telecom.TelecomSystem;
-
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * A VoipCallTransaction implementation that its sub transactions will be executed in serial
- */
-public class SerialTransaction extends VoipCallTransaction {
- public SerialTransaction(List<VoipCallTransaction> subTransactions,
- TelecomSystem.SyncRoot lock) {
- super(subTransactions, lock);
- }
-
- public void appendTransaction(VoipCallTransaction transaction){
- mSubTransactions.add(transaction);
- }
-
- @Override
- public void start() {
- // 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);
- }
- finish();
- return null;
- }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
- + ".s", mLock));
-
- if (mSubTransactions != null && mSubTransactions.size() > 0) {
- TransactionManager.TransactionCompleteListener subTransactionListener =
- new TransactionManager.TransactionCompleteListener() {
-
- @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();
- return null;
- }, new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode()
- + ".oTC", mLock));
- } else {
- if (mSubTransactions.size() > 0) {
- VoipCallTransaction transaction = mSubTransactions.remove(0);
- transaction.setCompleteListener(this);
- transaction.start();
- } else {
- scheduleTransaction();
- }
- }
- }
-
- @Override
- public void onTransactionTimeout(String transactionName) {
- handleTransactionFailure();
- CompletableFuture.completedFuture(null).thenApplyAsync(
- (x) -> {
- VoipCallTransactionResult mainResult =
- new VoipCallTransactionResult(
- VoipCallTransactionResult.RESULT_FAILED,
- String.format("sub transaction %s timed out",
- transactionName));
- mCompleteListener.onTransactionCompleted(mainResult,
- mTransactionName);
- finish();
- return null;
- }, new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode()
- + ".oTT", mLock));
- }
- };
- VoipCallTransaction transaction = mSubTransactions.remove(0);
- transaction.setCompleteListener(subTransactionListener);
- transaction.start();
- } else {
- scheduleTransaction();
- }
- }
-
- public void handleTransactionFailure() {}
-}
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
deleted file mode 100644
index 228bdde..0000000
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
-
-import android.os.OutcomeReceiver;
-import android.telecom.TelecomManager;
-import android.telecom.CallException;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Queue;
-
-public class TransactionManager {
- private static final String TAG = "VoipCallTransactionManager";
- private static TransactionManager INSTANCE = null;
- private static final Object sLock = new Object();
- private Queue<VoipCallTransaction> mTransactions;
- private VoipCallTransaction mCurrentTransaction;
-
- public interface TransactionCompleteListener {
- void onTransactionCompleted(VoipCallTransactionResult result, String transactionName);
- void onTransactionTimeout(String transactionName);
- }
-
- private TransactionManager() {
- mTransactions = new ArrayDeque<>();
- mCurrentTransaction = null;
- }
-
- public static TransactionManager getInstance() {
- synchronized (sLock) {
- if (INSTANCE == null) {
- INSTANCE = new TransactionManager();
- }
- }
- return INSTANCE;
- }
-
- @VisibleForTesting
- public static TransactionManager getTestInstance() {
- return new TransactionManager();
- }
-
- public void addTransaction(VoipCallTransaction transaction,
- OutcomeReceiver<VoipCallTransactionResult, CallException> receiver) {
- synchronized (sLock) {
- mTransactions.add(transaction);
- }
- transaction.setCompleteListener(new TransactionCompleteListener() {
- @Override
- public void onTransactionCompleted(VoipCallTransactionResult result,
- String transactionName){
- Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
- transactionName, result.getResult()));
- if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
- receiver.onResult(result);
- } else {
- receiver.onError(
- new CallException(result.getMessage(),
- result.getResult()));
- }
- finishTransaction();
- }
-
- @Override
- public void onTransactionTimeout(String transactionName){
- Log.i(TAG, String.format("transaction %s timeout", transactionName));
- receiver.onError(new CallException(transactionName + " timeout",
- CODE_OPERATION_TIMED_OUT));
- finishTransaction();
- }
- });
-
- startTransactions();
- }
-
- private void startTransactions() {
- synchronized (sLock) {
- if (mTransactions.isEmpty()) {
- // No transaction waiting for process
- return;
- }
-
- if (mCurrentTransaction != null) {
- // Ongoing transaction
- return;
- }
- mCurrentTransaction = mTransactions.poll();
- }
- mCurrentTransaction.start();
- }
-
- private void finishTransaction() {
- synchronized (sLock) {
- mCurrentTransaction = null;
- }
- startTransactions();
- }
-
- @VisibleForTesting
- public void clear() {
- List<VoipCallTransaction> pendingTransactions;
- synchronized (sLock) {
- pendingTransactions = new ArrayList<>(mTransactions);
- }
- for (VoipCallTransaction transaction : pendingTransactions) {
- transaction.finish();
- }
- }
-}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
deleted file mode 100644
index a952eb1..0000000
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.telecom.Log;
-
-import com.android.server.telecom.LoggedHandlerExecutor;
-import com.android.server.telecom.TelecomSystem;
-
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
-
-public class VoipCallTransaction {
- //TODO: add log events
- protected static final long TIMEOUT_LIMIT = 5000L;
- protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
- protected String mTransactionName = this.getClass().getSimpleName();
- private HandlerThread mHandlerThread;
- protected Handler mHandler;
- protected TransactionManager.TransactionCompleteListener mCompleteListener;
- protected List<VoipCallTransaction> mSubTransactions;
- protected TelecomSystem.SyncRoot mLock;
-
- public VoipCallTransaction(
- List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
- mSubTransactions = subTransactions;
- mHandlerThread = new HandlerThread(this.toString());
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
- mLock = lock;
- }
-
- public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
- this(null /** mSubTransactions */, lock);
- }
-
- public void start() {
- // 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);
- }
- finish();
- return null;
- }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
- + ".s", mLock));
-
- scheduleTransaction();
- }
-
- protected void scheduleTransaction() {
- LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
- mTransactionName + "@" + hashCode() + ".pT", 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();
- return null;
- }, executor)
- .exceptionallyAsync((throwable -> {
- Log.e(this, throwable, "Error while executing transaction.");
- return null;
- }), executor);
- }
-
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- return CompletableFuture.completedFuture(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null));
- }
-
- public void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
- mCompleteListener = listener;
- }
-
- public void finish() {
- // finish all sub transactions
- if (mSubTransactions != null && mSubTransactions.size() > 0) {
- mSubTransactions.forEach(VoipCallTransaction::finish);
- }
- mHandlerThread.quit();
- }
-}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
deleted file mode 100644
index 2916fc6..0000000
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.telecom.voip;
-
-import com.android.server.telecom.Call;
-
-import java.util.Objects;
-
-public class VoipCallTransactionResult {
- public static final int RESULT_SUCCEED = 0;
- public static final int RESULT_FAILED = 1;
-
- private int mResult;
- private String mMessage;
- private Call mCall;
-
- public VoipCallTransactionResult(int result, String message) {
- mResult = result;
- mMessage = message;
- }
-
- public VoipCallTransactionResult(int result, Call call, String message) {
- mResult = result;
- mCall = call;
- mMessage = message;
- }
-
- public int getResult() {
- return mResult;
- }
-
- public String getMessage() {
- return mMessage;
- }
-
- public Call getCall(){
- return mCall;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof VoipCallTransactionResult)) return false;
- VoipCallTransactionResult that = (VoipCallTransactionResult) o;
- return mResult == that.mResult && Objects.equals(mMessage, that.mMessage);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mResult, mMessage);
- }
-
- @Override
- public String toString() {
- return new StringBuilder().
- append("{ VoipCallTransactionResult: [mResult: ").
- append(mResult).
- append("], [mCall: ").
- append(mCall.toString()).
- append("], [mMessage=").
- append(mMessage).append("] }").toString();
- }
-}
diff --git a/testapps/Android.bp b/testapps/Android.bp
index 11ea474..45ea753 100644
--- a/testapps/Android.bp
+++ b/testapps/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 645a42b..e048f21 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -34,6 +34,7 @@
<uses-permission android:name="android.permission.REGISTER_CONNECTION_MANAGER"/>
<uses-permission android:name="android.permission.REGISTER_SIM_SUBSCRIPTION"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
<application android:label="@string/app_name">
<uses-library android:name="android.test.runner"/>
diff --git a/testapps/callaudiotest/Android.bp b/testapps/callaudiotest/Android.bp
index 81164e6..d996236 100644
--- a/testapps/callaudiotest/Android.bp
+++ b/testapps/callaudiotest/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/testapps/carmodedialer/Android.bp b/testapps/carmodedialer/Android.bp
index 9f65b8c..f142bf4 100644
--- a/testapps/carmodedialer/Android.bp
+++ b/testapps/carmodedialer/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/testapps/companionapp/Android.bp b/testapps/companionapp/Android.bp
index 8718b37..84ee4d3 100644
--- a/testapps/companionapp/Android.bp
+++ b/testapps/companionapp/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/testapps/res/layout/testdialer_main.xml b/testapps/res/layout/testdialer_main.xml
index 749d236..e4b5bef 100644
--- a/testapps/res/layout/testdialer_main.xml
+++ b/testapps/res/layout/testdialer_main.xml
@@ -54,6 +54,23 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancelMissedButton" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <EditText
+ android:id="@+id/set_composer_edit_text"
+ android:inputType="number"
+ android:layout_width="200dp"
+ android:layout_height="wrap_content" />
+ <Button
+ android:id="@+id/submit_composer_value"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/submitCallComposerLabel" />
+ </LinearLayout>
+
<CheckBox
android:id="@+id/call_with_rtt_checkbox"
android:layout_width="wrap_content"
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index b1a1f80..39c2deb 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -100,6 +100,8 @@
<string name="postCallActivityLabel">Test Post Call Screen</string>
+ <string name="submitCallComposerLabel">Set Call Composer</string>
+
<string-array name="rtt_mode_array">
<item>Full</item>
<item>HCO</item>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
index 1549443..ede06c6 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -82,6 +82,7 @@
* @param videoState The video state requested for the incoming call.
*/
public static void sendIncomingCallIntent(Context context, Uri handle, int videoState) {
+ TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
new ComponentName(context, TestConnectionService.class),
CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -94,10 +95,11 @@
extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
}
- TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+ telecomManager.addNewIncomingCall(phoneAccount, extras);
}
public static void sendIncomingRttCallIntent(Context context, Uri handle, int videoState) {
+ TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
new ComponentName(context, TestConnectionService.class),
CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -111,11 +113,12 @@
}
extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
- TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+ telecomManager.addNewIncomingCall(phoneAccount, extras);
}
public static void addNewUnknownCall(Context context, Uri handle, Bundle extras) {
Log.i(TAG, "Adding new unknown call with handle " + handle);
+ TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
new ComponentName(context, TestConnectionService.class),
CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -129,7 +132,7 @@
extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
}
- TelecomManager.from(context).addNewUnknownCall(phoneAccount, extras);
+ telecomManager.addNewUnknownCall(phoneAccount, extras);
}
public static void hangupCalls(Context context) {
diff --git a/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java b/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
index f33022c..d5ddc9b 100644
--- a/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.testapps;
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telecom.Log;
@@ -61,7 +62,7 @@
if (connection != null) {
connection.setConnectionDisconnected(DisconnectCause.INCOMING_REJECTED);
connection.destroy();
- TelecomManager tm = TelecomManager.from(this);
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
tm.showInCallScreen(false);
}
finish();
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index 273b060..4a7312c 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -123,6 +123,7 @@
public void registerPhoneAccount(Context context, ComponentName componentName, String id,
Uri address, String name, boolean areCallsLogged) {
+ TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
PhoneAccountHandle handle = new PhoneAccountHandle(componentName, id);
mPhoneAccounts.put(id, handle);
Bundle extras = new Bundle();
@@ -144,7 +145,7 @@
.setExtras(extras)
.setShortDescription(name);
- TelecomManager.from(context).registerPhoneAccount(builder.build());
+ telecomManager.registerPhoneAccount(builder.build());
}
public PhoneAccountHandle getPhoneAccountHandle(String id) {
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 5cdaf3d..708bae9 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -23,6 +23,7 @@
import android.app.NotificationManager;
import android.app.UiModeManager;
import android.app.role.RoleManager;
+import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.RingtoneManager;
@@ -190,7 +191,7 @@
}
private void placeOutgoingCall() {
- TelecomManager tm = TelecomManager.from(this);
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
if (mCheckIfPermittedBeforeCalling.isChecked()) {
@@ -215,7 +216,7 @@
}
private void placeSelfManagedOutgoingCall() {
- TelecomManager tm = TelecomManager.from(this);
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
if (mCheckIfPermittedBeforeCalling.isChecked()) {
@@ -233,14 +234,14 @@
}
private void initiateHandover() {
- TelecomManager tm = TelecomManager.from(this);
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
Uri address = Uri.parse(mNumber.getText().toString());
tm.acceptHandover(address, VideoProfile.STATE_BIDIRECTIONAL, phoneAccountHandle);
}
private void placeIncomingCall() {
- TelecomManager tm = TelecomManager.from(this);
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
if (mCheckIfPermittedBeforeCalling.isChecked()) {
@@ -263,7 +264,7 @@
}
private void placeSelfManagedIncomingCall() {
- TelecomManager tm = TelecomManager.from(this);
+ TelecomManager tm = this.getSystemService(TelecomManager.class);
PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
SelfManagedCallList.SELF_MANAGED_ACCOUNT_1A);
diff --git a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
index f17af2c..5a59c24 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
@@ -20,6 +20,7 @@
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
import android.telephony.ims.ImsRcsManager;
import android.util.Log;
import android.view.View;
@@ -30,9 +31,11 @@
import android.widget.Toast;
public class TestDialerActivity extends Activity {
+ private static final String TAG = TestDialerActivity.class.getSimpleName();
private static final int REQUEST_CODE_SET_DEFAULT_DIALER = 1;
private EditText mNumberView;
+ private EditText mCallComposerView;
private CheckBox mRttCheckbox;
private CheckBox mComposerCheckbox;
private EditText mPriorityView;
@@ -57,6 +60,13 @@
}
});
+ findViewById(R.id.submit_composer_value).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setCallComposer();
+ }
+ });
+
findViewById(R.id.place_call_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
@@ -79,6 +89,7 @@
});
mNumberView = (EditText) findViewById(R.id.number);
+ mCallComposerView = (EditText) findViewById(R.id.set_composer_edit_text);
mRttCheckbox = (CheckBox) findViewById(R.id.call_with_rtt_checkbox);
mComposerCheckbox = (CheckBox) findViewById(R.id.add_composer_attachments_checkbox);
findViewById(R.id.enable_car_mode).setOnClickListener(new OnClickListener() {
@@ -141,6 +152,23 @@
}
}
+ // Testers need a way of setting the call composer since this is currently not supported by
+ // Dialer. In the future, this will be a Dialer setting that users can enable/disable.
+ private void setCallComposer() {
+ final TelephonyManager telephonyManager =
+ (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+ String number = mCallComposerView.getText().toString();
+ try {
+ Log.i(TAG, "setCallComposer: value=[" + number + "]");
+ telephonyManager.setCallComposerStatus(Integer.parseInt(number));
+ Log.i(TAG, "setCallComposer: successfully set composer");
+ } catch (Exception e) {
+ Log.i(TAG, "setCallComposer: hit exception while setting the call composer."
+ + " See stack trace below for more info!");
+ e.printStackTrace();
+ }
+ }
+
private void placeCall() {
final TelecomManager telecomManager =
(TelecomManager) getSystemService(Context.TELECOM_SERVICE);
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index bdd4c1a..d2aca78 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -40,6 +40,8 @@
import android.widget.TextView;
import android.widget.Toast;
+import com.android.server.telecom.flags.Flags;
+
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -263,6 +265,8 @@
.getIntentExtras().getString(TelecomManager.EXTRA_CALL_SUBJECT);
boolean isBusiness = call.getDetails()
.getExtras().getBoolean(ImsCallProfile.EXTRA_IS_BUSINESS_CALL);
+ String businessName = call.getDetails()
+ .getExtras().getString(ImsCallProfile.EXTRA_ASSERTED_DISPLAY_NAME);
StringBuilder display = new StringBuilder();
display.append("priority=");
@@ -286,6 +290,7 @@
display.append(" subject=" + subject);
display.append(" isBusiness=" + isBusiness);
+ display.append(" businessName=" + businessName);
TextView attachmentsTextView = findViewById(R.id.incoming_composer_attachments);
attachmentsTextView.setText(display.toString());
break;
diff --git a/testapps/streamingtest/Android.bp b/testapps/streamingtest/Android.bp
index bd0a582..8d5cd6c 100644
--- a/testapps/streamingtest/Android.bp
+++ b/testapps/streamingtest/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/testapps/transactionalVoipApp/Android.bp b/testapps/transactionalVoipApp/Android.bp
index 68089e2..8bac8f1 100644
--- a/testapps/transactionalVoipApp/Android.bp
+++ b/testapps/transactionalVoipApp/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_telecom",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/testapps/transactionalVoipApp/res/values-bs/strings.xml b/testapps/transactionalVoipApp/res/values-bs/strings.xml
index 24ffba2..f417043 100644
--- a/testapps/transactionalVoipApp/res/values-bs/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-bs/strings.xml
@@ -31,7 +31,7 @@
<string name="request_earpiece_endpoint" msgid="6649571985089296573">"Slušalica"</string>
<string name="request_speaker_endpoint" msgid="1033259535289845405">"Zvučnik"</string>
<string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
- <string name="start_stream" msgid="3567634786280097431">"pokreni prijenos"</string>
+ <string name="start_stream" msgid="3567634786280097431">"pokreni prenos"</string>
<string name="crash_app" msgid="2548690390730057704">"izbaci izuzetak"</string>
<string name="update_notification" msgid="8677916482672588779">"ažuriraj obavještenje u stil poziva u toku"</string>
</resources>
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/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
index d58aa13..414caa8 100644
--- a/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
@@ -20,7 +20,7 @@
<string name="app_name" msgid="2907804426411305091">"Activité de test de l\'API transactionnelle"</string>
<string name="in_call_activity_name" msgid="7545884666442897585">"Activité transactionnelle durant l\'appel"</string>
<string name="register_phone_account" msgid="1920315963082350332">"Inscrire un compte téléphonique"</string>
- <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + application en arrière-plan)"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + appli en arrière-plan)"</string>
<string name="start_outgoing" msgid="1441644037370361864">"Démarrer un appel sortant"</string>
<string name="start_incoming" msgid="6444983300186361271">"Démarrer un appel entrant"</string>
<string name="get_call_id" msgid="5513943242738347108">"identifiant de l\'appel non défini"</string>
diff --git a/testapps/transactionalVoipApp/res/values-in/strings.xml b/testapps/transactionalVoipApp/res/values-in/strings.xml
index 935f036..ba41376 100644
--- a/testapps/transactionalVoipApp/res/values-in/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-in/strings.xml
@@ -27,7 +27,7 @@
<string name="set_call_active" msgid="3365404393507589899">"setelAktif"</string>
<string name="answer" msgid="5423590397665409939">"jawab"</string>
<string name="set_call_inactive" msgid="7106775211368705195">"setelNonaktif"</string>
- <string name="disconnect_call" msgid="1349412380315371385">"putuskan koneksi"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"berhenti hubungkan"</string>
<string name="request_earpiece_endpoint" msgid="6649571985089296573">"Earpiece"</string>
<string name="request_speaker_endpoint" msgid="1033259535289845405">"Speaker"</string>
<string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
diff --git a/testapps/transactionalVoipApp/res/values-sq/strings.xml b/testapps/transactionalVoipApp/res/values-sq/strings.xml
index ddaba66..2816473 100644
--- a/testapps/transactionalVoipApp/res/values-sq/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-sq/strings.xml
@@ -30,7 +30,7 @@
<string name="disconnect_call" msgid="1349412380315371385">"shkëput"</string>
<string name="request_earpiece_endpoint" msgid="6649571985089296573">"Receptori"</string>
<string name="request_speaker_endpoint" msgid="1033259535289845405">"Altoparlanti"</string>
- <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth-i"</string>
<string name="start_stream" msgid="3567634786280097431">"nis transmetimin"</string>
<string name="crash_app" msgid="2548690390730057704">"gjenero një përjashtim"</string>
<string name="update_notification" msgid="8677916482672588779">"përditëso njoftimin me stilin e telefonatës në vazhdim"</string>
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 4ca6030..04dcef8 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -24,6 +24,7 @@
android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
<!-- TODO: Needed because we call BluetoothAdapter.getDefaultAdapter() statically, and
BluetoothAdapter is a final class. -->
<uses-permission android:name="android.permission.BLUETOOTH" />
@@ -44,10 +45,13 @@
<!-- Used to access PlatformCompat APIs -->
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
-
+
<!-- Used to register NotificationListenerService -->
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
+ <!-- Used to query the audio framework to determine if a notification sound should play. -->
+ <uses-permission android:name="android.permission.QUERY_AUDIO_STATE"/>
+
<application android:label="@string/app_name"
android:debuggable="true">
<uses-library android:name="android.test.runner" />
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
index 5da7f31..9caf0b5 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -25,8 +25,8 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -47,11 +47,11 @@
import android.telecom.VideoProfile;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.Analytics;
@@ -87,6 +87,7 @@
super.setUp();
// this is a mock
mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+ when(mSubscriptionManager.createForAllUserProfiles()).thenReturn(mSubscriptionManager);
when(mSubscriptionManager.getActiveSubscriptionInfoList())
.thenReturn(Collections.emptyList());
}
@@ -381,7 +382,7 @@
waitForHandlerAction(
mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(),
+ .getCallAudioRouteAdapter().getAdapterHandler(),
TEST_TIMEOUT);
waitForHandlerAction(
mTelecomSystem.getCallsManager().getCallAudioManager()
@@ -391,7 +392,7 @@
mInCallServiceFixtureX.getInCallAdapter().setAudioRoute(CallAudioState.ROUTE_SPEAKER, null);
waitForHandlerAction(
mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(),
+ .getCallAudioRouteAdapter().getAdapterHandler(),
TEST_TIMEOUT);
waitForHandlerAction(
mTelecomSystem.getCallsManager().getCallAudioManager()
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 51c3b33..7646c2d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.RES_BLOCK_STATUS;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_BLOCKED_IN_LIST;
import static com.android.server.telecom.tests.ConnectionServiceFixture.STATUS_HINTS_EXTRA;
import static org.junit.Assert.assertEquals;
@@ -24,11 +26,11 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
+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.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
@@ -39,22 +41,20 @@
import android.content.Context;
import android.content.IContentProvider;
-import android.content.pm.PackageManager;
-import android.media.AudioDeviceInfo;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.graphics.drawable.Icon;
+import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
-import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.telecom.Call;
import android.telecom.CallAudioState;
+import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
@@ -65,14 +65,13 @@
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
import androidx.test.filters.FlakyTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import com.android.internal.telecom.IInCallAdapter;
-import android.telecom.CallerInfo;
import com.google.common.base.Predicate;
@@ -110,6 +109,7 @@
doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
mPackageManager = mContext.getPackageManager();
when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
}
@Override
@@ -626,6 +626,48 @@
@LargeTest
@Test
+ public void testIncomingThenOutgoingCalls_AssociatedUsersNotEqual() throws Exception {
+ when(mFeatureFlags.associatedUserRefactorForWorkProfile()).thenReturn(true);
+ InCallServiceFixture.setIgnoreOverrideAdapterFlag(true);
+
+ // Receive incoming call via mPhoneAccountMultiUser
+ IdPair incoming = startAndMakeActiveIncomingCall("650-555-2323",
+ mPhoneAccountMultiUser.getAccountHandle(), mConnectionServiceFixtureA);
+ waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+ TEST_TIMEOUT);
+ // Make outgoing call on mPhoneAccountMultiUser (unassociated sim to simulate guest/
+ // secondary user scenario where both MO/MT calls exist).
+ IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountMultiUser.getAccountHandle(), mConnectionServiceFixtureA);
+ waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+ TEST_TIMEOUT);
+
+ // Outgoing call should be on hold while incoming call is made active
+ mConnectionServiceFixtureA.mConnectionById.get(incoming.mConnectionId).state =
+ Connection.STATE_HOLDING;
+
+ // Swap calls and verify that outgoing call is now the active call while the incoming call
+ // is the held call.
+ mConnectionServiceFixtureA.sendSetOnHold(outgoing.mConnectionId);
+ waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+ TEST_TIMEOUT);
+ assertEquals(Call.STATE_HOLDING,
+ mInCallServiceFixtureX.getCall(outgoing.mCallId).getState());
+ assertEquals(Call.STATE_ACTIVE,
+ mInCallServiceFixtureX.getCall(incoming.mCallId).getState());
+
+ // Ensure no issues with call disconnect.
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(incoming.mCallId);
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(outgoing.mCallId);
+ assertEquals(Call.STATE_DISCONNECTING,
+ mInCallServiceFixtureX.getCall(incoming.mCallId).getState());
+ assertEquals(Call.STATE_DISCONNECTING,
+ mInCallServiceFixtureX.getCall(outgoing.mCallId).getState());
+ InCallServiceFixture.setIgnoreOverrideAdapterFlag(false);
+ }
+
+ @LargeTest
+ @Test
public void testAudioManagerOperations() throws Exception {
AudioManager audioManager = (AudioManager) mComponentContextFixture.getTestDouble()
.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
@@ -648,15 +690,15 @@
mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER, null);
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+ .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor =
ArgumentCaptor.forClass(AudioDeviceInfo.class);
- verify(audioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
- infoArgumentCaptor.capture());
+ verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1))
+ .setCommunicationDevice(infoArgumentCaptor.capture());
assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, infoArgumentCaptor.getValue().getType());
mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE, null);
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+ .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
// setSpeakerPhoneOn(false) gets called once during the call initiation phase
verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1))
.clearCommunicationDevice();
@@ -667,7 +709,7 @@
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
.getCallAudioModeStateMachine().getHandler(), TEST_TIMEOUT);
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+ .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
verify(audioManager, timeout(TEST_TIMEOUT))
.abandonAudioFocusForCall();
verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce())
@@ -847,8 +889,7 @@
@Override
public Bundle answer(InvocationOnMock invocation) throws Throwable {
Bundle bundle = new Bundle();
- bundle.putInt(BlockedNumberContract.RES_BLOCK_STATUS,
- BlockedNumberContract.STATUS_BLOCKED_IN_LIST);
+ bundle.putInt(RES_BLOCK_STATUS, STATUS_BLOCKED_IN_LIST);
return bundle;
}
});
@@ -1197,7 +1238,7 @@
.getState());
mInCallServiceFixtureX.mInCallAdapter.mute(true);
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+ .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
assertTrue(mTelecomSystem.getCallsManager().getAudioState().isMuted());
// Make an emergency call.
@@ -1206,14 +1247,14 @@
assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(emergencyCall.mCallId)
.getState());
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+ .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
// Should be unmute automatically.
assertFalse(mTelecomSystem.getCallsManager().getAudioState().isMuted());
// Toggle mute during an emergency call.
mTelecomSystem.getCallsManager().getCallAudioManager().toggleMute();
waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
- .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+ .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
// Should keep unmute.
assertFalse(mTelecomSystem.getCallsManager().getAudioState().isMuted());
@@ -1341,7 +1382,6 @@
public void testValidateStatusHintsImage_addExistingConnection() throws Exception {
IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1214",
mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
- Connection existingConnection = mConnectionServiceFixtureA.mLatestConnection;
// Modify existing connection with StatusHints image exploit
Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
index a98c1ee..a706f4b 100644
--- a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
@@ -16,12 +16,14 @@
package com.android.server.telecom.tests;
-import static android.provider.BlockedNumberContract.STATUS_BLOCKED_IN_LIST;
-import static android.provider.BlockedNumberContract.STATUS_NOT_BLOCKED;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_BLOCKED_IN_LIST;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_NOT_BLOCKED;
import static junit.framework.TestCase.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@@ -32,11 +34,10 @@
import android.os.Bundle;
import android.os.PersistableBundle;
import android.provider.CallLog;
-import android.telecom.CallerInfo;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallerInfoLookupHelper;
@@ -51,7 +52,6 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
@@ -88,14 +88,14 @@
super.setUp();
when(mCall.getHandle()).thenReturn(TEST_HANDLE);
mFilter = new BlockCheckerFilter(mContext, mCall, mCallerInfoLookupHelper,
- mBlockCheckerAdapter);
+ mBlockCheckerAdapter, mFeatureFlags);
}
@SmallTest
@Test
public void testBlockNumber() throws Exception {
when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
- eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+ eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
.thenReturn(STATUS_BLOCKED_IN_LIST);
setEnhancedBlockingEnabled(false);
@@ -107,7 +107,7 @@
@Test
public void testBlockNumberWhenEnhancedBlockingEnabled() throws Exception {
when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
- eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+ eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
.thenReturn(STATUS_BLOCKED_IN_LIST);
setEnhancedBlockingEnabled(true);
@@ -121,7 +121,7 @@
@Test
public void testDontBlockNumber() throws Exception {
when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
- eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+ eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
.thenReturn(STATUS_NOT_BLOCKED);
setEnhancedBlockingEnabled(false);
@@ -133,7 +133,7 @@
@Test
public void testDontBlockNumberWhenEnhancedBlockingEnabled() throws Exception {
when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
- eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+ eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
.thenReturn(STATUS_NOT_BLOCKED);
setEnhancedBlockingEnabled(true);
diff --git a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
index 56cb735..57aee62 100644
--- a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
+++ b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
@@ -16,16 +16,21 @@
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;
import android.os.UserHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.settings.BlockedNumbersActivity;
import com.android.server.telecom.settings.BlockedNumbersUtil;
import org.junit.Before;
@@ -57,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 943aac1..ac4a94e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -18,20 +18,37 @@
import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
import android.content.Intent;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Parcel;
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -44,23 +61,11 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.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 java.util.Arrays;
+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 {
@@ -73,10 +78,12 @@
@Mock BluetoothLeAudio mBluetoothLeAudio;
@Mock AudioManager mockAudioManager;
@Mock AudioDeviceInfo mSpeakerInfo;
+ @Mock Executor mExecutor;
BluetoothDeviceManager mBluetoothDeviceManager;
BluetoothProfile.ServiceListener serviceListenerUnderTest;
BluetoothStateReceiver receiverUnderTest;
+ CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
ArgumentCaptor<BluetoothLeAudio.Callback> leAudioCallbacksTest;
private BluetoothDevice device1;
@@ -104,10 +111,14 @@
when(mBluetoothHearingAid.getHiSyncId(device4)).thenReturn(100L);
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
- mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter);
+ mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+ mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter,
+ mCommunicationDeviceTracker, mFeatureFlags);
mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager);
+ mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
mockAudioManager = mContext.getSystemService(AudioManager.class);
+ mExecutor = mContext.getMainExecutor();
ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
@@ -115,7 +126,8 @@
serviceCaptor.capture(), eq(BluetoothProfile.HEADSET));
serviceListenerUnderTest = serviceCaptor.getValue();
- receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager, mRouteManager);
+ receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager,
+ mRouteManager, mCommunicationDeviceTracker, mFeatureFlags);
mBluetoothDeviceManager.setHeadsetServiceForTesting(mBluetoothHeadset);
mBluetoothDeviceManager.setHearingAidServiceForTesting(mBluetoothHearingAid);
@@ -126,6 +138,8 @@
verify(mBluetoothLeAudio).registerCallback(any(), leAudioCallbacksTest.capture());
when(mSpeakerInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false);
+ when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
}
@Override
@@ -395,7 +409,8 @@
when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
- verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+ verify(mAdapter).setActiveDevice(eq(device1),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL));
mBluetoothDeviceManager.disconnectAudio();
@@ -413,8 +428,8 @@
when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
- AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
- when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID);
+ AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(device5.getAddress(),
+ AudioDeviceInfo.TYPE_HEARING_AID);
List<AudioDeviceInfo> devices = new ArrayList<>();
devices.add(mockAudioDeviceInfo);
@@ -434,7 +449,7 @@
when(mockAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
mBluetoothDeviceManager.disconnectAudio();
- verify(mockAudioManager).clearCommunicationDevice();
+ verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice();
}
@SmallTest
@@ -448,8 +463,8 @@
when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
- AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
- when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+ AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(device5.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
List<AudioDeviceInfo> devices = new ArrayList<>();
devices.add(mockAudioDeviceInfo);
@@ -470,7 +485,7 @@
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
mBluetoothDeviceManager.disconnectAudio();
- verify(mockAudioManager).clearCommunicationDevice();
+ verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice();
}
@SmallTest
@@ -508,7 +523,86 @@
@SmallTest
@Test
+ public void testConnectMultipleLeAudioDevices() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ receiverUnderTest.setIsInCall(true);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device1, 1);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device2, 1);
+ when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ AudioDeviceInfo leAudioDevice2 = createMockAudioDeviceInfo(device2.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ devices.add(leAudioDevice1);
+ devices.add(leAudioDevice2);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+ .thenReturn(true);
+
+ // Connect LE audio device
+ mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
+ verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ verify(mBluetoothHeadset, never()).connectAudio();
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ // Verify that we set the communication device for device 1
+ verify(mockAudioManager).setCommunicationDevice(leAudioDevice1);
+
+ // Change active device to other LE audio device
+ receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device2,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+
+ // Verify call to clearLeAudioCommunicationDevice
+ verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
+ // Verify that we set the communication device for device2
+ verify(mockAudioManager).setCommunicationDevice(leAudioDevice2);
+ }
+
+ @SmallTest
+ @Test
+ public void testClearCommunicationDeviceOnActiveDeviceChange() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ receiverUnderTest.setIsInCall(true);
+
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ devices.add(leAudioDevice1);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+ .thenReturn(true);
+
+ // Pretend that the speaker device is currently the requested device set for communication.
+ // This test ensures that the set/clear communication logic for audio switching in/out of BT
+ // is properly working when the receiver processes an active device change intent.
+ mCommunicationDeviceTracker.setTestCommunicationDevice(TYPE_BUILTIN_SPEAKER);
+
+ // Notify that LE audio device has been turned on
+ receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device1,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ // Verify call to clear speaker communication device
+ verify(mockAudioManager).clearCommunicationDevice();
+ // Verify that LE audio communication device was set after clearing the speaker device
+ verify(mockAudioManager).setCommunicationDevice(leAudioDevice1);
+ }
+
+ @SmallTest
+ @Test
public void testConnectDualModeEarbud() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
receiverUnderTest.setIsInCall(true);
// LE Audio earbuds connected
@@ -527,11 +621,11 @@
when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
- AudioDeviceInfo mockAudioDevice5Info = mock(AudioDeviceInfo.class);
- when(mockAudioDevice5Info.getAddress()).thenReturn(device5.getAddress());
+ AudioDeviceInfo mockAudioDevice5Info = createMockAudioDeviceInfo(device5.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ AudioDeviceInfo mockAudioDevice6Info = createMockAudioDeviceInfo(device6.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
when(mockAudioDevice5Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
- AudioDeviceInfo mockAudioDevice6Info = mock(AudioDeviceInfo.class);
- when(mockAudioDevice6Info.getAddress()).thenReturn(device6.getAddress());
when(mockAudioDevice6Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
List<AudioDeviceInfo> devices = new ArrayList<>();
devices.add(mockAudioDevice5Info);
@@ -572,6 +666,7 @@
verify(mBluetoothHeadset, never()).connectAudio();
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mockAudioManager, times(1)).clearCommunicationDevice();
// Reconnect other LE Audio earbud
devices.add(mockAudioDevice5Info);
@@ -582,7 +677,7 @@
// Disconnects audio
mBluetoothDeviceManager.disconnectAudio();
- verify(mockAudioManager, times(1)).clearCommunicationDevice();
+ verify(mockAudioManager, times(2)).clearCommunicationDevice();
verify(mBluetoothHeadset, times(1)).disconnectAudio();
// TEST 2: HFP preferred for DUPLEX
@@ -592,7 +687,8 @@
eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL))).thenReturn(true);
mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
verify(mAdapter).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
- verify(mAdapter, times(1)).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ verify(mAdapter, times(1)).setActiveDevice(device5,
+ BluetoothAdapter.ACTIVE_DEVICE_ALL);
verify(mBluetoothHeadset).connectAudio();
mBluetoothDeviceManager.disconnectAudio();
verify(mBluetoothHeadset, times(2)).disconnectAudio();
@@ -600,23 +696,46 @@
@SmallTest
@Test
- public void testClearHearingAidCommunicationDevice() {
- AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
- when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1);
- when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID);
- List<AudioDeviceInfo> devices = new ArrayList<>();
- devices.add(mockAudioDeviceInfo);
+ public void testClearHearingAidCommunicationDeviceLegacy() {
+ assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_HEARING_AID);
+ }
- when(mockAudioManager.getAvailableCommunicationDevices())
- .thenReturn(devices);
- when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
- .thenReturn(true);
+ @SmallTest
+ @Test
+ public void testClearHearingAidCommunicationDeviceWithFlag() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_HEARING_AID);
+ }
- mBluetoothDeviceManager.setHearingAidCommunicationDevice();
- when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
- mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
- verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
- assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
+ @SmallTest
+ @Test
+ public void testClearLeAudioCommunicationDeviceLegacy() {
+ assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_BLE_HEADSET);
+ }
+
+ @SmallTest
+ @Test
+ public void testClearLeAudioCommunicationDeviceWithFlag() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_BLE_HEADSET);
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectedDevicesDoNotContainDuplicateDevices() {
+ BluetoothDevice hfpDevice = mock(BluetoothDevice.class);
+ when(hfpDevice.getAddress()).thenReturn("00:00:00:00:00:00");
+ when(hfpDevice.getType()).thenReturn(BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
+ BluetoothDevice leDevice = mock(BluetoothDevice.class);
+ when(hfpDevice.getAddress()).thenReturn("00:00:00:00:00:00");
+ when(hfpDevice.getType()).thenReturn(BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
+
+ mBluetoothDeviceManager.onDeviceConnected(hfpDevice,
+ BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
+ mBluetoothDeviceManager.onDeviceConnected(leDevice,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
+
+ assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
}
@SmallTest
@@ -626,22 +745,92 @@
when(mBluetoothLeAudio.isInbandRingtoneEnabled(1)).thenReturn(true);
when(mBluetoothLeAudio.getGroupId(eq(device3))).thenReturn(1);
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
- BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
- receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
- BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
- receiverUnderTest.onReceive(mContext,
buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device3, 1);
- when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
when(mRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(device3);
when(mRouteManager.isCachedLeAudioDevice(eq(device3))).thenReturn(true);
- assertEquals(3, mBluetoothDeviceManager.getNumConnectedDevices());
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
+ when(mRouteManager.getMostRecentlyReportedActiveDevice()).thenReturn(device3);
+ assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
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
+ ) {
+ AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+ when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1);
+ when(mockAudioDeviceInfo.getType()).thenReturn(device_type);
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(mockAudioDeviceInfo);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
+ .thenReturn(true);
+
+ if (flagEnabled) {
+ BluetoothDevice btDevice = device_type == AudioDeviceInfo.TYPE_BLE_HEADSET
+ ? device1 : null;
+ mCommunicationDeviceTracker.setCommunicationDevice(device_type, btDevice);
+ } else {
+ if (device_type == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+ mBluetoothDeviceManager.setLeAudioCommunicationDevice();
+ } else {
+ mBluetoothDeviceManager.setHearingAidCommunicationDevice();
+ }
+ }
+ when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
+ if (flagEnabled) {
+ mCommunicationDeviceTracker.clearCommunicationDevice(device_type);
+ assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType(device_type));
+ } else {
+ if (device_type == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+ mBluetoothDeviceManager.clearLeAudioCommunicationDevice();
+ assertFalse(mBluetoothDeviceManager.isLeAudioCommunicationDevice());
+ } else {
+ mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
+ assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
+ }
+ }
+ verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
+ }
+
+ private AudioDeviceInfo createMockAudioDeviceInfo(String address, int audioType) {
+ AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+ when(mockAudioDeviceInfo.getType()).thenReturn(audioType);
+ if (address != null) {
+ when(mockAudioDeviceInfo.getAddress()).thenReturn(address);
+ }
+ return mockAudioDeviceInfo;
+ }
+
private Intent buildConnectionActionIntent(int state, BluetoothDevice device, int deviceType) {
String intentString;
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 1a6fb88..1c885c1 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -16,6 +16,17 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+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;
+import static org.mockito.Mockito.when;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -26,9 +37,11 @@
import android.content.ContentResolver;
import android.os.Parcel;
import android.telecom.Log;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
@@ -46,23 +59,20 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class BluetoothRouteManagerTest extends TelecomTestCase {
private static final int TEST_TIMEOUT = 1000;
static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01");
static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02");
static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03");
- static final BluetoothDevice HEARING_AID_DEVICE = makeBluetoothDevice("00:00:00:00:00:04");
+ static final BluetoothDevice HEARING_AID_DEVICE_LEFT = makeBluetoothDevice("CA:FE:DE:CA:00:01");
+ static final BluetoothDevice HEARING_AID_DEVICE_RIGHT =
+ makeBluetoothDevice("CA:FE:DE:CA:00:02");
+ // See HearingAidService#getActiveDevices
+ // Note: It is really important that the left HA is the first one. The left HA is always
+ // in the first index (0) and the right one in the second index (1).
+ static final BluetoothDevice[] HEARING_AIDS =
+ new BluetoothDevice[]{HEARING_AID_DEVICE_LEFT, HEARING_AID_DEVICE_RIGHT};
@Mock private BluetoothAdapter mBluetoothAdapter;
@Mock private BluetoothDeviceManager mDeviceManager;
@@ -71,6 +81,7 @@
@Mock private BluetoothLeAudio mBluetoothLeAudio;
@Mock private Timeouts.Adapter mTimeoutsAdapter;
@Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+ @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Override
@Before
@@ -86,6 +97,59 @@
@SmallTest
@Test
+ public void testConnectLeftHearingAidWhenLeftIsActive() {
+ BluetoothRouteManager sm = setupStateMachine(
+ BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_LEFT);
+ sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
+ BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
+ when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
+ when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
+
+ setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
+ when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
+ .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+
+ executeRoutingAction(sm,
+ BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress());
+
+ executeRoutingAction(sm,
+ BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress());
+
+ assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+ + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName());
+
+ sm.quitNow();
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectRightHearingAidWhenLeftIsActive() {
+ BluetoothRouteManager sm = setupStateMachine(
+ BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_RIGHT);
+ sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
+ BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
+ when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
+ when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
+
+
+ setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
+ when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
+ .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+
+ executeRoutingAction(sm,
+ BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress());
+
+ executeRoutingAction(sm,
+ BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress());
+
+ assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+ + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName());
+
+ sm.quitNow();
+ }
+
+ @SmallTest
+ @Test
public void testConnectBtRetryWhileNotConnected() {
BluetoothRouteManager sm = setupStateMachine(
BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
@@ -112,15 +176,15 @@
BluetoothRouteManager sm = setupStateMachine(
BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
setupConnectedDevices(new BluetoothDevice[]{DEVICE1},
- new BluetoothDevice[]{HEARING_AID_DEVICE}, new BluetoothDevice[]{DEVICE2},
- DEVICE1, HEARING_AID_DEVICE, DEVICE2);
+ HEARING_AIDS, new BluetoothDevice[]{DEVICE2},
+ DEVICE1, HEARING_AIDS, DEVICE2);
sm.onActiveDeviceChanged(DEVICE1, BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
sm.onActiveDeviceChanged(DEVICE2, BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
- sm.onActiveDeviceChanged(HEARING_AID_DEVICE,
+ sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress());
- verifyConnectionAttempt(HEARING_AID_DEVICE, 0);
+ verifyConnectionAttempt(HEARING_AID_DEVICE_LEFT, 0);
verifyConnectionAttempt(DEVICE1, 0);
verifyConnectionAttempt(DEVICE2, 0);
assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
@@ -171,11 +235,71 @@
sm.quitNow();
}
+ @SmallTest
+ @Test
+ public void testSkipInactiveBtDeviceWhenEvaluateActualState() {
+ BluetoothRouteManager sm = setupStateMachine(
+ BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, HEARING_AID_DEVICE_LEFT);
+ setupConnectedDevices(null, HEARING_AIDS,
+ null, null, HEARING_AIDS, null);
+ executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST,
+ HEARING_AID_DEVICE_LEFT.getAddress());
+ assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
+ sm.quitNow();
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectBtWithoutAddress_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);
+ setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null,
+ null);
+ when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+ nullable(ContentResolver.class))).thenReturn(0L);
+ when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN);
+ executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, null);
+ // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction,
+ // so wait twice for the retry attempt, again to make sure there are only three attempts,
+ // and once more for good luck.
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+ // 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());
+ sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
+ sm.quitNow();
+ }
+
private BluetoothRouteManager setupStateMachine(String initialState,
BluetoothDevice initialDevice) {
resetMocks();
BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
- new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
+ new TelecomSystem.SyncRoot() { }, mDeviceManager,
+ mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags);
sm.setListener(mListener);
sm.setInitialStateForTesting(initialState, initialDevice);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -185,10 +309,11 @@
private void setupConnectedDevices(BluetoothDevice[] hfpDevices,
BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices,
- BluetoothDevice hfpActiveDevice, BluetoothDevice hearingAidActiveDevice,
+ BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices,
BluetoothDevice leAudioDevice) {
if (hfpDevices == null) hfpDevices = new BluetoothDevice[]{};
if (hearingAidDevices == null) hearingAidDevices = new BluetoothDevice[]{};
+ if (hearingAidActiveDevices == null) hearingAidActiveDevices = new BluetoothDevice[]{};
if (leAudioDevice == null) leAudioDevices = new BluetoothDevice[]{};
when(mDeviceManager.getNumConnectedDevices()).thenReturn(
@@ -207,7 +332,7 @@
when(mBluetoothHearingAid.getConnectedDevices())
.thenReturn(Arrays.asList(hearingAidDevices));
when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEARING_AID)))
- .thenReturn(Arrays.asList(hearingAidActiveDevice, null));
+ .thenReturn(Arrays.asList(hearingAidActiveDevices));
when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
.thenReturn(Arrays.asList(leAudioDevice, null));
}
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
index 0f9ffc1..c546c3f 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
@@ -16,6 +16,21 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE1;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE2;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE3;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.executeRoutingAction;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -25,9 +40,11 @@
import android.bluetooth.BluetoothStatusCodes;
import android.content.ContentResolver;
import android.telecom.Log;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
@@ -45,23 +62,8 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Stream;
import java.util.stream.Collectors;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE1;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE2;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE3;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.executeRoutingAction;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(Parameterized.class)
public class BluetoothRouteTransitionTests extends TelecomTestCase {
private enum ListenerUpdate {
@@ -263,6 +265,7 @@
@Mock private BluetoothLeAudio mBluetoothLeAudio;
@Mock private Timeouts.Adapter mTimeoutsAdapter;
@Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+ @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Override
@Before
@@ -416,7 +419,8 @@
when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
nullable(ContentResolver.class))).thenReturn(100000L);
BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
- new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
+ new TelecomSystem.SyncRoot() { }, mDeviceManager,
+ mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags);
sm.setListener(mListener);
sm.setInitialStateForTesting(initialState, initialDevice);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
index 7e197fe..d608d0a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
@@ -27,6 +27,7 @@
import android.content.ComponentName;
import android.net.Uri;
+import android.os.UserHandle;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -44,6 +45,7 @@
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.ui.ToastFactory;
import org.junit.After;
@@ -89,6 +91,7 @@
@Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
@Mock private EmergencyCallDiagnosticLogger mMockEmergencyCallDiagnosticLogger;
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
@Override
@Before
@@ -121,8 +124,10 @@
doReturn(new ComponentName(mContext, CallTest.class))
.when(mMockConnectionService).getComponentName();
mCallAnomalyWatchdog = new CallAnomalyWatchdog(mTestScheduledExecutorService, mLock,
- mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger);
+ mFeatureFlags, mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger,
+ mMockTelecomMetricsController);
mCallAnomalyWatchdog.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
}
@Override
@@ -862,6 +867,7 @@
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
}
}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 3d06ad0..d1a3eb6 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -16,26 +16,50 @@
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;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+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.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
import android.telecom.DisconnectCause;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.SparseArray;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioModeStateMachine;
import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs;
+import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
-import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.DtmfLocalTonePlayer;
+import com.android.server.telecom.InCallController;
import com.android.server.telecom.InCallTonePlayer;
-import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
import com.android.server.telecom.RingbackPlayer;
import com.android.server.telecom.Ringer;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
+import com.android.server.telecom.flags.FeatureFlags;
import org.junit.After;
import org.junit.Before;
@@ -48,27 +72,13 @@
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
-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;
-
@RunWith(JUnit4.class)
public class CallAudioManagerTest extends TelecomTestCase {
@Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ @Mock private InCallController mInCallController;
@Mock private CallsManager mCallsManager;
@Mock private CallAudioModeStateMachine mCallAudioModeStateMachine;
@Mock private InCallTonePlayer.Factory mPlayerFactory;
@@ -78,6 +88,8 @@
@Mock private BluetoothStateReceiver mBluetoothStateReceiver;
@Mock private TelecomSystem.SyncRoot mLock;
+ @Mock private FeatureFlags mFlags;
+
private CallAudioManager mCallAudioManager;
@Override
@@ -87,12 +99,15 @@
doAnswer((invocation) -> {
InCallTonePlayer mockInCallTonePlayer = mock(InCallTonePlayer.class);
doAnswer((invocation2) -> {
- mCallAudioManager.setIsTonePlaying(true);
+ mCallAudioManager.setIsTonePlaying(invocation.getArgument(0), true);
return true;
}).when(mockInCallTonePlayer).startTone();
return mockInCallTonePlayer;
- }).when(mPlayerFactory).createPlayer(anyInt());
+ }).when(mPlayerFactory).createPlayer(any(Call.class), anyInt());
when(mCallsManager.getLock()).thenReturn(mLock);
+ when(mCallsManager.getInCallController()).thenReturn(mInCallController);
+ when(mInCallController.getBtBindingFuture(any(Call.class))).thenReturn(null);
+ when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(true);
mCallAudioManager = new CallAudioManager(
mCallAudioRouteStateMachine,
mCallsManager,
@@ -101,7 +116,8 @@
mRinger,
mRingbackPlayer,
mBluetoothStateReceiver,
- mDtmfLocalTonePlayer);
+ mDtmfLocalTonePlayer,
+ mFlags);
}
@Override
@@ -204,7 +220,7 @@
assertMessageArgEquality(correctArgs, captor.getValue());
disconnectCall(call);
- stopTone();
+ stopTone(call);
mCallAudioManager.onCallRemoved(call);
verifyProperCleanup();
@@ -241,7 +257,7 @@
mCallAudioManager.onCallStateChanged(call, CallState.ANSWERED, CallState.ACTIVE);
disconnectCall(call);
- stopTone();
+ stopTone(call);
mCallAudioManager.onCallRemoved(call);
verifyProperCleanup();
@@ -277,25 +293,40 @@
verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
assertMessageArgEquality(expectedArgs, captor.getValue());
- verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
- anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
-
+ if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+ // Expect another invocation due to audio mode change signal.
+ verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+ anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ } else {
+ verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+ anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ }
when(call.getState()).thenReturn(CallState.ACTIVE);
mCallAudioManager.onCallStateChanged(call, CallState.DIALING, CallState.ACTIVE);
verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
assertMessageArgEquality(expectedArgs, captor.getValue());
- verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
- anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
-
+ if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+ verify(mCallAudioModeStateMachine, times(4)).sendMessageWithArgs(
+ anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ } else {
+ verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+ anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ }
disconnectCall(call);
- stopTone();
+ stopTone(call);
mCallAudioManager.onCallRemoved(call);
verifyProperCleanup();
}
+ @Test
+ public void testSingleOutgoingCallWithoutAudioModeUpdateOnForegroundCallChange() {
+ when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(false);
+ testSingleOutgoingCall();
+ }
+
@MediumTest
@Test
public void testRingbackStartStop() {
@@ -327,8 +358,14 @@
verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
assertMessageArgEquality(expectedArgs, captor.getValue());
- verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
- anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+ // Expect an extra time due to audio mode change signal
+ verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+ anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ } else {
+ verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+ anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+ }
// Ensure we started ringback.
verify(mRingbackPlayer).startRingbackForCall(any(Call.class));
@@ -350,6 +387,12 @@
verify(mRingbackPlayer, times(1)).startRingbackForCall(any(Call.class));
}
+ @Test
+ public void testRingbackStartStopWithoutAudioModeUpdateOnForegroundCallChange() {
+ when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(false);
+ testRingbackStartStop();
+ }
+
@SmallTest
@Test
public void testNewCallGoesToAudioProcessing() {
@@ -389,9 +432,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(
@@ -495,7 +541,7 @@
mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
CallState.DISCONNECTED);
- verify(mPlayerFactory, never()).createPlayer(anyInt());
+ verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
CallAudioModeStateMachine.MessageArgs expectedArgs2 = new Builder()
.setHasActiveOrDialingCalls(false)
.setHasRingingCalls(false)
@@ -522,11 +568,15 @@
Call call = createAudioProcessingCall();
+
when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+ handleWaitForBtIcsBinding(call);
mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
CallState.SIMULATED_RINGING);
- verify(mPlayerFactory, never()).createPlayer(anyInt());
+ 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)
.setHasRingingCalls(true)
@@ -555,7 +605,7 @@
mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
CallState.ACTIVE);
- verify(mPlayerFactory, never()).createPlayer(anyInt());
+ verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
.setHasActiveOrDialingCalls(true)
.setHasRingingCalls(false)
@@ -584,7 +634,7 @@
mCallAudioManager.onCallStateChanged(call, CallState.SIMULATED_RINGING,
CallState.ACTIVE);
- verify(mPlayerFactory, never()).createPlayer(anyInt());
+ verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
.setHasActiveOrDialingCalls(true)
.setHasRingingCalls(false)
@@ -643,7 +693,7 @@
mCallAudioManager.onCallStateChanged(call, CallState.SIMULATED_RINGING,
CallState.DISCONNECTED);
- verify(mPlayerFactory, never()).createPlayer(anyInt());
+ verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
CallAudioModeStateMachine.MessageArgs expectedArgs2 = new Builder()
.setHasActiveOrDialingCalls(false)
.setHasRingingCalls(false)
@@ -702,12 +752,86 @@
assertFalse(captor.getValue().isStreaming);
}
+ @SmallTest
+ @Test
+ public void testTriggerAudioManagerModeChange() {
+ if (!mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+ // Skip if the new behavior isn't in use.
+ return;
+ }
+ // Start with an incoming PSTN call
+ Call pstnCall = mock(Call.class);
+ when(pstnCall.getState()).thenReturn(CallState.RINGING);
+ when(pstnCall.getIsVoipAudioMode()).thenReturn(false);
+ ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
+
+ // Add the call
+ mCallAudioManager.onCallAdded(pstnCall);
+ verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+ eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE), captor.capture());
+ CallAudioModeStateMachine.MessageArgs expectedArgs =
+ new Builder()
+ .setHasActiveOrDialingCalls(false)
+ .setHasRingingCalls(true)
+ .setHasHoldingCalls(false)
+ .setIsTonePlaying(false)
+ .setHasAudioProcessingCalls(false)
+ .setForegroundCallIsVoip(false)
+ .setSession(null)
+ .setForegroundCallIsVoip(false)
+ .build();
+ assertMessageArgEquality(expectedArgs, captor.getValue());
+ clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+ // Make call active; don't expect there to be an audio mode transition.
+ when(pstnCall.getState()).thenReturn(CallState.ACTIVE);
+ mCallAudioManager.onCallStateChanged(pstnCall, CallState.RINGING, CallState.ACTIVE);
+ verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(
+ eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE),
+ any(CallAudioModeStateMachine.MessageArgs.class));
+ clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+ // Add a new Voip call in ringing state; this should not result in a direct audio mode
+ // change.
+ Call voipCall = mock(Call.class);
+ when(voipCall.getState()).thenReturn(CallState.RINGING);
+ when(voipCall.getIsVoipAudioMode()).thenReturn(true);
+ mCallAudioManager.onCallAdded(voipCall);
+ verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(
+ eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE),
+ any(CallAudioModeStateMachine.MessageArgs.class));
+ clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+ // Make voip call active and set the PSTN call to locally disconnecting; the new foreground
+ // call will be the voip call.
+ when(pstnCall.isLocallyDisconnecting()).thenReturn(true);
+ when(voipCall.getState()).thenReturn(CallState.ACTIVE);
+ mCallAudioManager.onCallStateChanged(voipCall, CallState.RINGING, CallState.ACTIVE);
+ verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+ eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE), captor.capture());
+ CallAudioModeStateMachine.MessageArgs expectedArgs2 =
+ new Builder()
+ .setHasActiveOrDialingCalls(true)
+ .setHasRingingCalls(false)
+ .setHasHoldingCalls(false)
+ .setIsTonePlaying(false)
+ .setHasAudioProcessingCalls(false)
+ .setForegroundCallIsVoip(false)
+ .setSession(null)
+ .setForegroundCallIsVoip(true)
+ .build();
+ assertMessageArgEquality(expectedArgs2, captor.getValue());
+ }
+
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());
@@ -733,8 +857,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);
@@ -765,7 +892,7 @@
"", "", "", ToneGenerator.TONE_PROP_PROMPT));
mCallAudioManager.onCallStateChanged(call, CallState.ACTIVE, CallState.DISCONNECTED);
- verify(mPlayerFactory).createPlayer(InCallTonePlayer.TONE_CALL_ENDED);
+ verify(mPlayerFactory).createPlayer(any(Call.class), eq(InCallTonePlayer.TONE_CALL_ENDED));
correctArgs = new Builder()
.setHasActiveOrDialingCalls(false)
.setHasRingingCalls(false)
@@ -782,10 +909,10 @@
assertMessageArgEquality(correctArgs, captor.getValue());
}
- private void stopTone() {
+ private void stopTone(Call call) {
ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
- mCallAudioManager.setIsTonePlaying(false);
+ mCallAudioManager.setIsTonePlaying(call, false);
CallAudioModeStateMachine.MessageArgs correctArgs = new Builder()
.setHasActiveOrDialingCalls(false)
.setHasRingingCalls(false)
@@ -819,4 +946,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/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index d7854a5..9414e16 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -16,14 +16,27 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioModeStateMachine;
-import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
+import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.SystemStateHelper;
import org.junit.After;
@@ -33,16 +46,6 @@
import org.junit.runners.JUnit4;
import org.mockito.Mock;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class CallAudioModeStateMachineTest extends TelecomTestCase {
private static final int TEST_TIMEOUT = 1000;
@@ -51,6 +54,7 @@
@Mock private AudioManager mAudioManager;
@Mock private CallAudioManager mCallAudioManager;
@Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
private HandlerThread mTestThread;
@@ -60,8 +64,9 @@
mTestThread = new HandlerThread("CallAudioModeStateMachineTest");
mTestThread.start();
super.setUp();
- when(mCallAudioManager.getCallAudioRouteStateMachine())
+ when(mCallAudioManager.getCallAudioRouteAdapter())
.thenReturn(mCallAudioRouteStateMachine);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
}
@Override
@@ -76,7 +81,7 @@
@Test
public void testNoFocusWhenRingerSilenced() throws Throwable {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -108,7 +113,7 @@
@Test
public void testSwitchToStreamingMode() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -138,7 +143,7 @@
@Test
public void testExitStreamingMode() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ENTER_STREAMING_FOCUS_FOR_TESTING);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -166,7 +171,7 @@
@Test
public void testNoRingWhenDeviceIsAtEar() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
sm.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL, new Builder()
@@ -202,7 +207,7 @@
@Test
public void testRegainFocusWhenHfpIsConnectedSilenced() throws Throwable {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -246,7 +251,7 @@
@Test
public void testDoNotRingTwiceWhenHfpConnected() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -284,7 +289,7 @@
@Test
public void testStartRingingAfterHfpConnectedIfNotAlreadyPlaying() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -321,4 +326,16 @@
private void resetMocks() {
clearInvocations(mCallAudioManager, mAudioManager);
}
+
+ private boolean areAudioFocusRequestsMatch(AudioFocusRequest r1, AudioFocusRequest r2) {
+ if ((r1 == null) || (r2 == null)) {
+ return false;
+ }
+
+ if (r1.getFocusGain() != r2.getFocusGain()) {
+ return false;
+ }
+
+ return r1.getAudioAttributes().equals(r2.getAudioAttributes());
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
index c7e5aa9..844a216 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
@@ -16,10 +16,22 @@
package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+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.verify;
+import static org.mockito.Mockito.when;
+
import android.media.AudioManager;
import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioModeStateMachine;
import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs;
@@ -36,15 +48,6 @@
import java.util.Collection;
import java.util.List;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyInt;
-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.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(Parameterized.class)
public class CallAudioModeTransitionTests extends TelecomTestCase {
private static class ModeTestParameters {
@@ -103,6 +106,7 @@
@Mock private SystemStateHelper mSystemStateHelper;
@Mock private AudioManager mAudioManager;
@Mock private CallAudioManager mCallAudioManager;
+ @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
private final ModeTestParameters mParams;
private HandlerThread mTestThread;
@@ -130,13 +134,14 @@
@SmallTest
public void modeTransitionTest() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
- mAudioManager, mTestThread.getLooper());
+ mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
sm.setCallAudioManager(mCallAudioManager);
sm.sendMessage(mParams.initialAudioState);
waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
resetMocks();
when(mCallAudioManager.startRinging()).thenReturn(true);
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
if (mParams.initialAudioState
== CallAudioModeStateMachine.ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING) {
when(mAudioManager.getMode())
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
new file mode 100644
index 0000000..809abb4
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -0,0 +1,1247 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.tests;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.ACTIVE_FOCUS;
+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;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.NO_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.RINGING_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+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_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+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 android.util.Pair;
+
+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.CallAudioRouteStateMachine;
+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 com.android.server.telecom.metrics.TelecomMetricsController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class CallAudioRouteControllerTest extends TelecomTestCase {
+ private CallAudioRouteController mController;
+ @Mock WiredHeadsetManager mWiredHeadsetManager;
+ @Mock AudioManager mAudioManager;
+ @Mock AudioDeviceInfo mEarpieceDeviceInfo;
+ @Mock CallsManager mCallsManager;
+ @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;
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
+ private AudioRoute mEarpieceRoute;
+ private AudioRoute mSpeakerRoute;
+ private boolean mOverrideSpeakerToBus;
+ private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
+ private static final BluetoothDevice BLUETOOTH_DEVICE_1 =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
+ private static final Set<BluetoothDevice> BLUETOOTH_DEVICES;
+ static {
+ BLUETOOTH_DEVICES = new HashSet<>();
+ BLUETOOTH_DEVICES.add(BLUETOOTH_DEVICE_1);
+ }
+ private static final int TEST_TIMEOUT = 500;
+ AudioRoute.Factory mAudioRouteFactory = new AudioRoute.Factory() {
+ @Override
+ public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
+ AudioManager audioManager) {
+ if (mOverrideSpeakerToBus && type == AudioRoute.TYPE_SPEAKER) {
+ type = AudioRoute.TYPE_BUS;
+ }
+ return new AudioRoute(type, bluetoothAddress, mAudioDeviceInfo);
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ when(mWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+ when(mEarpieceDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
+ new AudioDeviceInfo[] {
+ mEarpieceDeviceInfo
+ });
+ 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(mCallsManager.getForegroundCall()).thenReturn(mCall);
+ 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,
+ mockStatusBarNotifier, mFeatureFlags, mMockTelecomMetricsController);
+ mController.setAudioRouteFactory(mAudioRouteFactory);
+ mController.setAudioManager(mAudioManager);
+ mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
+ mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+ mOverrideSpeakerToBus = false;
+ mController.setCallAudioManager(mCallAudioManager);
+ when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
+ when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
+ when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(true);
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(false);
+ when(mFeatureFlags.newAudioPathSpeakerBroadcastAndUnfocusedRouting()).thenReturn(false);
+ when(mFeatureFlags.fixUserRequestBaselineRouteVideoCall()).thenReturn(false);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mController.getAdapterHandler().getLooper().quit();
+ mController.getAdapterHandler().getLooper().getThread().join();
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testInitializeWithEarpiece() {
+ mController.initialize();
+ assertEquals(mEarpieceRoute, mController.getCurrentRoute());
+ assertEquals(2, mController.getAvailableRoutes().size());
+ assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+ }
+
+ @SmallTest
+ @Test
+ public void testInitializeWithoutEarpiece() {
+ when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
+ new AudioDeviceInfo[] {});
+
+ mController.initialize();
+ assertEquals(mSpeakerRoute, mController.getCurrentRoute());
+ }
+
+ @SmallTest
+ @Test
+ public void testInitializeWithWiredHeadset() {
+ AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+ when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+ mController.initialize();
+ 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, 0);
+ // 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_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testActiveFocusAudioRouting() {
+ mController.initialize();
+ // Connect wired headset
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Explicitly switch to speaker
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ // Expect that active focus received from a new active call will force route to baseline
+ // (in this case, this should be the wired headset).
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Switch back to speaker and send active focus for end tone to confirm that audio routing
+ // doesn't fall back onto the baseline.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testVideoCallHoldRouteToEarpiece() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ // 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, 0);
+ // 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,
+ BLUETOOTH_DEVICE_1);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
+ AudioRoute.TYPE_BLUETOOTH_SCO);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
+ when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
+
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ assertFalse(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+ .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+ assertTrue(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ assertTrue(mController.isActive());
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+ // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+ // audio focus is relinquished.
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+
+ // Ensure the BT device is disconnected.
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
+ assertFalse(mController.isActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectWiredHeadset() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectAndDisconnectDock() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+ 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));
+
+ mController.sendMessageWithSessionInfo(DISCONNECT_DOCK);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSpeakerToggle() {
+ mController.initialize();
+ mController.setActive(true);
+ 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));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSpeakerToggleWhenDockConnected() {
+ mController.initialize();
+ mController.setActive(true);
+ mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+ 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));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ 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));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchEarpiece() {
+ mController.initialize();
+ 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));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_EARPIECE);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testSwitchBluetooth() {
+ doAnswer(invocation -> {
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ return true;
+ }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+
+ mController.initialize();
+ mController.setActive(true);
+ 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));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0,
+ BLUETOOTH_DEVICE_1.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ 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 testSwitchSpeakerAndHeadset() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(USER_SWITCH_HEADSET);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testEnableAndDisableStreaming() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(STREAMING_FORCE_ENABLED);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_STREAMING,
+ CallAudioState.ROUTE_STREAMING, null, new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ mController.sendMessageWithSessionInfo(STREAMING_FORCE_DISABLED);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @SmallTest
+ @Test
+ public void testStreamRingMuteChange() {
+ mController.initialize();
+
+ // Make sure we register a receiver for the STREAM_MUTE_CHANGED_ACTION so we can see if the
+ // ring stream unmutes.
+ ArgumentCaptor<BroadcastReceiver> brCaptor = ArgumentCaptor.forClass(
+ BroadcastReceiver.class);
+ ArgumentCaptor<IntentFilter> filterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
+ verify(mContext, times(3)).registerReceiver(brCaptor.capture(), filterCaptor.capture());
+ boolean foundValid = false;
+ for (int ix = 0; ix < brCaptor.getAllValues().size(); ix++) {
+ BroadcastReceiver receiver = brCaptor.getAllValues().get(ix);
+ IntentFilter filter = filterCaptor.getAllValues().get(ix);
+ if (!filter.hasAction(AudioManager.STREAM_MUTE_CHANGED_ACTION)) {
+ continue;
+ }
+
+ // Fake out a call to the broadcast receiver and make sure we call into audio manager
+ // to trigger re-evaluation of ringing.
+ Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+ intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+ intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_RING);
+ receiver.onReceive(mContext, intent);
+ verify(mCallAudioManager).onRingerModeChange();
+ foundValid = true;
+ }
+ assertTrue(foundValid);
+ }
+
+
+ @SmallTest
+ @Test
+ public void testToggleMute() 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));
+
+ when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+ mController.sendMessageWithSessionInfo(MUTE_OFF);
+ 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)).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, 0);
+ 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));
+ // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+ // audio focus is relinquished.
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+ }
+
+ @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));
+ }
+
+ @SmallTest
+ @Test
+ public void testRouteFromBtSwitchInRingingSelected() {
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+ when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+ when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
+
+ 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));
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+ assertFalse(mController.isActive());
+
+ // BT device should be cached. Verify routing into BT device once focus becomes active.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0,
+ BLUETOOTH_DEVICE_1.getAddress());
+ 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(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ 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 testUpdateRouteForForeground() {
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Ensure that supported routes is updated along with the current route to reflect the
+ // foreground call's supported audio routes.
+ when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_SPEAKER);
+ mController.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ assertEquals(3, mController.getAvailableRoutes().size());
+ assertEquals(1, mController.getCallSupportedRoutes().size());
+ }
+
+ @SmallTest
+ @Test
+ public void testRouteToBusForAuto() {
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+ .thenReturn(new AudioDeviceInfo[0]);
+ mOverrideSpeakerToBus = true;
+ mController.initialize();
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+ PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+ assertEquals(AudioRoute.TYPE_BUS, pendingRoute.getDestRoute().getType());
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_SPEAKER, null, new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Ensure that turning speaker phone on doesn't get triggered when speaker isn't available.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ verify(mockStatusBarNotifier, times(0)).notifySpeakerphone(anyBoolean());
+
+ }
+
+ @SmallTest
+ @Test
+ public void testMimicVoiceDialWithBt() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ mController.initialize();
+ mController.setActive(true);
+
+ 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));
+
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ // Mimic behavior of controller processing BT_AUDIO_DISCONNECTED
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
+ // Process BT_AUDIO_CONNECTED from connecting to BT device in active focus request.
+ mController.setIsScoAudioConnected(true);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+ // Verify SCO not disconnected and route stays on connected BT device.
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).times(0)).disconnectSco();
+ 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 testTransactionalCallBtConnectingAndSwitchCallEndpoint() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ mController.initialize();
+ mController.setActive(true);
+
+ 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));
+
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ // Omit sending BT_AUDIO_CONNECTED to mimic scenario where BT is still connecting and user
+ // switches to speaker.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+ BLUETOOTH_DEVICE_1);
+
+ // Verify SCO disconnected
+ verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+ // Verify audio properly routes into speaker.
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @Test
+ @SmallTest
+ public void testBluetoothRouteToActiveDevice() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ // Connect first BT device.
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ // Connect another BT device.
+ String scoDeviceAddress = "00:00:00:00:00:03";
+ BluetoothDevice scoDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
+ BLUETOOTH_DEVICES.add(scoDevice);
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ scoDevice);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDeviceAddress);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+ BLUETOOTH_DEVICE_1);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+ scoDevice);
+ CallAudioState 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));
+
+ // Mimic behavior when inactive headset is used to answer the call (i.e. tap headset). In
+ // this case, the inactive BT device will become the active device (reported to us from BT
+ // stack to controller via BT_ACTIVE_DEVICE_PRESENT).
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BLUETOOTH_DEVICE_1.getAddress());
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+ scoDevice);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+ BLUETOOTH_DEVICE_1);
+ // Verify audio routed to BLUETOOTH_DEVICE_1
+ 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));
+
+ // Now switch call to active focus so that base route can be recalculated.
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ // Verify that audio is still routed into BLUETOOTH_DEVICE_1 and not the 2nd BT device.
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Clean up BLUETOOTH_DEVICES for subsequent tests.
+ BLUETOOTH_DEVICES.remove(scoDevice);
+ }
+
+ @Test
+ @SmallTest
+ public void verifyRouteReinitializedAfterCallEnd() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ mController.initialize();
+ mController.setActive(true);
+
+ // Switch to speaker
+ 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 call audio route is reinitialized to default (in this case, earpiece) when
+ // call audio focus is lost.
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ }
+
+ @Test
+ @SmallTest
+ public void testUserSwitchBaselineRouteVideoCall() {
+ when(mFeatureFlags.fixUserRequestBaselineRouteVideoCall()).thenReturn(true);
+ mController.initialize();
+ mController.setActive(true);
+ // Set capabilities for video call.
+ when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+
+ // Turn on speaker
+ 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));
+
+ // USER_SWITCH_BASELINE_ROUTE (explicit user request). Verify that audio is routed back to
+ // earpiece.
+ mController.sendMessageWithSessionInfo(USER_SWITCH_BASELINE_ROUTE,
+ CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE);
+ mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+ new HashSet<>());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // SWITCH_BASELINE_ROUTE. Verify that audio is routed to speaker for non-user requests.
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE);
+ mController.sendMessageWithSessionInfo(SPEAKER_ON);
+ 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));
+ }
+
+ @Test
+ @SmallTest
+ public void testRouteToWatchWhenCallAnsweredOnWatch_MultipleBtDevices() {
+ when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+ // Connect first BT device.
+ verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+ // Connect another BT device.
+ String scoDeviceAddress = "00:00:00:00:00:03";
+ BluetoothDevice watchDevice =
+ BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
+ when(mBluetoothRouteManager.isWatch(eq(watchDevice))).thenReturn(true);
+ BLUETOOTH_DEVICES.add(watchDevice);
+
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ watchDevice);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Signal that watch is now the active device. This is done in BluetoothStateReceiver and
+ // then BT_ACTIVE_DEVICE_PRESENT will be sent to the controller to be processed.
+ mController.updateActiveBluetoothDevice(
+ new Pair<>(AudioRoute.TYPE_BLUETOOTH_SCO, watchDevice.getAddress()));
+ // Emulate scenario with call answered on watch. Ensure at this point that audio was routed
+ // into watch
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+ 0, watchDevice);
+ mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED,
+ 0, BLUETOOTH_DEVICE_1);
+ expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH, watchDevice, BLUETOOTH_DEVICES);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Hardcode signal from BT stack signaling to Telecom that watch is now the active device.
+ // This should just be a no-op since audio was already routed when processing active focus.
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, scoDeviceAddress);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ // Mimic behavior of controller processing BT_AUDIO_DISCONNECTED for BLUETOOTH_DEVICE_1 and
+ // verify that audio remains routed to the watch and not routed to earpiece (this should
+ // be taking into account what the BT active device is as reported to us by the BT stack).
+ mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+ INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+
+ BLUETOOTH_DEVICES.remove(watchDevice);
+ }
+
+
+ @Test
+ @SmallTest
+ public void testAbandonCallAudioFocusAfterCallEnd() {
+ // Make sure in-band ringing is disabled so that route never becomes active
+ when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
+
+ mController.initialize();
+ mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+ BLUETOOTH_DEVICE_1);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+ mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+ AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+ verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+ any(CallAudioState.class), eq(expectedState));
+ assertFalse(mController.isActive());
+
+ // Verify route never went active due to in-band ringing being disabled.
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+ assertFalse(mController.isActive());
+
+ // Emulate scenario of rejecting an incoming call so that call focus is lost and verify
+ // that we abandon the call audio focus that was gained from when the call went to
+ // ringing state.
+ mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+ // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+ // audio focus is relinquished.
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+ }
+
+ 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/CallAudioRoutePeripheralAdapterTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
index 2fc6ec6..79247be 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
@@ -24,7 +24,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.AsyncRingtonePlayer;
import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 431a253..e97de2e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -16,6 +16,27 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.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;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -26,18 +47,20 @@
import android.media.IAudioService;
import android.os.HandlerThread;
import android.telecom.CallAudioState;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
-import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ConnectionServiceWrapper;
-import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.StatusBarNotifier;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import org.junit.After;
import org.junit.Before;
@@ -50,6 +73,7 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -57,25 +81,6 @@
import java.util.List;
import java.util.Set;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class CallAudioRouteStateMachineTest extends TelecomTestCase {
@@ -102,6 +107,7 @@
private AudioManager mockAudioManager;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
private HandlerThread mThreadHandler;
+ CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Override
@Before
@@ -112,6 +118,8 @@
mThreadHandler.start();
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+ mCommunicationDeviceTracker.setBluetoothRouteManager(mockBluetoothRouteManager);
mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
@Override
@@ -131,9 +139,12 @@
when(fakeSelfManagedCall.isAlive()).thenReturn(true);
when(fakeSelfManagedCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
when(fakeSelfManagedCall.isSelfManaged()).thenReturn(true);
+ when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(false);
doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
any(CallAudioState.class));
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false);
}
@Override
@@ -156,7 +167,9 @@
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
mThreadHandler.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
// Since we don't know if we're on a platform with an earpiece or not, all we can do
// is ensure the stateMachine construction didn't fail. But at least we exercised the
@@ -175,7 +188,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
@@ -198,8 +214,8 @@
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// assert expected end state
assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
@@ -209,6 +225,112 @@
.onCallAudioStateChanged(any(), any());
}
+ @SmallTest
+ @Test
+ public void testSystemAudioStateIsNotUpdatedFlagOff() {
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
+ when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls);
+ when(mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()).thenReturn(false);
+
+ // start state --> ROUTE_EARPIECE
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ stateMachine.initialize(initState);
+
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+ CallAudioRouteStateMachine.ROUTE_EARPIECE);
+
+ // ROUTE_EARPIECE --> ROUTE_SPEAKER
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+ CallAudioRouteStateMachine.SPEAKER_ON);
+
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+ waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+ CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState();
+
+ // assert expected end state
+ assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+ CallAudioRouteStateMachine.ROUTE_SPEAKER);
+ // should update the audio route on all tracked calls ...
+ verify(mockConnectionServiceWrapper, times(trackedCalls.size()))
+ .onCallAudioStateChanged(any(), any());
+
+ assertNotEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState());
+ }
+
+ @SmallTest
+ @Test
+ public void testSystemAudioStateIsUpdatedFlagOn() {
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
+ when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls);
+ when(mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()).thenReturn(true);
+
+ // start state --> ROUTE_EARPIECE
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ stateMachine.initialize(initState);
+
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+ CallAudioRouteStateMachine.ROUTE_EARPIECE);
+
+ // ROUTE_EARPIECE --> ROUTE_SPEAKER
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+ CallAudioRouteStateMachine.SPEAKER_ON);
+
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState();
+
+ // assert expected end state
+ assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+ CallAudioRouteStateMachine.ROUTE_SPEAKER);
+ // should update the audio route on all tracked calls ...
+ verify(mockConnectionServiceWrapper, times(trackedCalls.size()))
+ .onCallAudioStateChanged(any(), any());
+
+ assertEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState());
+ }
+
@MediumTest
@Test
public void testStreamRingMuteChange() {
@@ -220,7 +342,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -264,7 +389,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -287,14 +415,14 @@
CallAudioState expectedMiddleState = new CallAudioState(false,
CallAudioState.ROUTE_WIRED_HEADSET,
CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(initState, expectedMiddleState);
resetMocks();
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(expectedMiddleState, initState);
}
@@ -309,7 +437,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -330,7 +461,7 @@
CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(initState, expectedEndState);
resetMocks();
stateMachine.sendMessageWithSessionInfo(
@@ -338,7 +469,7 @@
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
}
@@ -353,7 +484,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1);
@@ -378,12 +512,12 @@
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
null, availableDevices);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(initState, expectedMidState);
// clear out the handler state before resetting mocks in order to avoid introducing a
// CallAudioState that has a null list of supported BT devices
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
resetMocks();
// Now, switch back to BT explicitly
@@ -401,9 +535,9 @@
CallAudioState.ROUTE_BLUETOOTH,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
bluetoothDevice1, availableDevices);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// second wait needed for the BT_AUDIO_CONNECTED message
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(expectedMidState, expectedEndState);
stateMachine.sendMessageWithSessionInfo(
@@ -413,9 +547,9 @@
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// second wait needed for the BT_AUDIO_CONNECTED message
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// Verify that we're still on bluetooth.
assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
}
@@ -431,7 +565,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -446,13 +583,13 @@
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.RINGING_FOCUS);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verify(mockBluetoothRouteManager, never()).connectBluetoothAudio(nullable(String.class));
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.ACTIVE_FOCUS);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verify(mockBluetoothRouteManager, times(1)).connectBluetoothAudio(nullable(String.class));
}
@@ -467,7 +604,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
setInBandRing(false);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -481,7 +621,7 @@
CallAudioRouteStateMachine.RINGING_FOCUS);
// Wait for the state machine to finish transiting to ActiveEarpiece before hooking up
// bluetooth mocks
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
when(mockBluetoothRouteManager.getConnectedDevices())
@@ -490,7 +630,7 @@
CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED);
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verify(mockBluetoothRouteManager, never()).connectBluetoothAudio(null);
CallAudioState expectedEndState = new CallAudioState(false,
@@ -501,14 +641,17 @@
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.ACTIVE_FOCUS);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verify(mockBluetoothRouteManager, times(1)).connectBluetoothAudio(null);
when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
.thenReturn(bluetoothDevice1);
stateMachine.sendMessage(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
- verify(mockCallAudioManager, times(1)).onRingerModeChange();
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ // It is possible that this will be called twice from ActiveBluetoothRoute#enter. The extra
+ // call to setBluetoothOn will trigger BT_AUDIO_CONNECTED, which also ends up invoking
+ // CallAudioManager#onRingerModeChange.
+ verify(mockCallAudioManager, atLeastOnce()).onRingerModeChange();
}
@SmallTest
@@ -522,7 +665,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
List<BluetoothDevice> availableDevices =
Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3);
@@ -548,17 +694,80 @@
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH,
0, bluetoothDevice2.getAddress());
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verify(mockBluetoothRouteManager).connectBluetoothAudio(bluetoothDevice2.getAddress());
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
CallAudioState expectedEndState = new CallAudioState(false,
CallAudioState.ROUTE_BLUETOOTH,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
bluetoothDevice2,
availableDevices);
- verifyNewSystemCallAudioState(initState, expectedEndState);
+ assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
+ }
+
+ @SmallTest
+ @Test
+ public void testCallDisconnectedWhenAudioRoutedToBluetooth() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+ List<BluetoothDevice> availableDevices = Arrays.asList(bluetoothDevice1);
+
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
+ when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+ when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
+ when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices);
+ when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true);
+ when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
+ doAnswer(invocation -> {
+ when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+ .thenReturn(bluetoothDevice1);
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ return null;
+ }).when(mockBluetoothRouteManager).connectBluetoothAudio(bluetoothDevice1.getAddress());
+ doAnswer(invocation -> {
+ when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+ .thenReturn(bluetoothDevice1);
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
+ return null;
+ }).when(mockBluetoothRouteManager).disconnectAudio();
+
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, null,
+ availableDevices);
+ stateMachine.initialize(initState);
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.NO_FOCUS, bluetoothDevice1.getAddress());
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ verify(mockBluetoothRouteManager).disconnectAudio();
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ CallAudioState expectedEndState = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
+ bluetoothDevice1,
+ availableDevices);
+
+ assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
}
@SmallTest
@@ -572,7 +781,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -581,13 +793,13 @@
// Raise a dock connect event.
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.CONNECT_DOCK);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
assertTrue(!stateMachine.isInActiveState());
verify(mockAudioManager, never()).setSpeakerphoneOn(eq(true));
// Raise a dock disconnect event.
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.DISCONNECT_DOCK);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
assertTrue(!stateMachine.isInActiveState());
verify(mockAudioManager, never()).setSpeakerphoneOn(eq(false));
}
@@ -603,7 +815,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
@@ -615,7 +830,7 @@
// Switch to active, pretending that a call came in.
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.ACTIVE_FOCUS);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// Make sure that we've successfully switched to the active speaker route and that we've
// called setSpeakerOn
@@ -637,7 +852,10 @@
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- Runnable::run /** do async stuff sync for test purposes */);
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
List<BluetoothDevice> availableDevices =
Arrays.asList(bluetoothDevice1, bluetoothDevice2);
@@ -659,7 +877,7 @@
// Switch to active, pretending that a call came in.
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.ACTIVE_FOCUS);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// Make sure that we've successfully switched to the active BT route and that we've
// called connectAudio on the right device.
@@ -670,6 +888,112 @@
@SmallTest
@Test
+ public void testSetAndClearEarpieceCommunicationDevice() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ AudioDeviceInfo earpiece = mock(AudioDeviceInfo.class);
+ when(earpiece.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ when(earpiece.getAddress()).thenReturn("");
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(earpiece);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(eq(earpiece)))
+ .thenReturn(true);
+ when(mockAudioManager.getCommunicationDevice()).thenReturn(earpiece);
+
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER |
+ CallAudioState.ROUTE_WIRED_HEADSET);
+ stateMachine.initialize(initState);
+
+ // Switch to active
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ // Make sure that we've successfully switched to the active earpiece and that we set the
+ // communication device.
+ assertTrue(stateMachine.isInActiveState());
+ ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+ AudioDeviceInfo.class);
+ verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+ assertEquals(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+ infoArgumentCaptor.getValue().getType());
+
+ // Route earpiece to speaker
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+ CallAudioRouteStateMachine.SPEAKER_ON);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ // Assert that communication device was cleared
+ verify(mockAudioManager).clearCommunicationDevice();
+ }
+
+ @SmallTest
+ @Test
+ public void testSetAndClearWiredHeadsetCommunicationDevice() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ }
+
+ @SmallTest
+ @Test
+ public void testSetAndClearUsbHeadsetCommunicationDevice() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_USB_HEADSET);
+ }
+
+ @SmallTest
+ @Test
+ public void testActiveFocusRouteSwitchFromQuiescentBluetooth() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ // Start the route in quiescent and ensure that a switch to ACTIVE_FOCUS transitions to
+ // the corresponding active route even when there aren't any active BT devices available.
+ CallAudioState initState = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE);
+ stateMachine.initialize(initState);
+
+ // Switch to active
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ // Make sure that we've successfully switched to the active route on BT
+ assertTrue(stateMachine.isInActiveState());
+ }
+
+ @SmallTest
+ @Test
public void testInitializationWithEarpieceNoHeadsetNoBluetooth() {
CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -753,7 +1077,9 @@
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
mThreadHandler.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.initialize();
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
}
@@ -770,7 +1096,9 @@
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
mThreadHandler.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
@@ -781,12 +1109,12 @@
CallAudioState expectedEndState = new CallAudioState(false,
CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_STREAMING);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(initState, expectedEndState);
resetMocks();
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
assertEquals(initState, stateMachine.getCurrentCallAudioState());
}
@@ -806,7 +1134,9 @@
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
mThreadHandler.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -834,6 +1164,8 @@
@MediumTest
@Test
public void testIgnoreImplicitBTSwitchWhenDeviceIsWatch() {
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
mContext,
mockCallsManager,
@@ -843,9 +1175,23 @@
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
mThreadHandler.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
+ AudioDeviceInfo headset = mock(AudioDeviceInfo.class);
+ when(headset.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(headset.getAddress()).thenReturn("");
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(headset);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(eq(headset)))
+ .thenReturn(true);
+ when(mockAudioManager.getCommunicationDevice()).thenReturn(headset);
+
CallAudioState initState = new CallAudioState(false,
CallAudioState.ROUTE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET
| CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
@@ -854,10 +1200,14 @@
// Switch to active
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
CallAudioRouteStateMachine.ACTIVE_FOCUS);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// Make sure that we've successfully switched to the active headset.
assertTrue(stateMachine.isInActiveState());
+ ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+ AudioDeviceInfo.class);
+ verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+ assertEquals(AudioDeviceInfo.TYPE_WIRED_HEADSET, infoArgumentCaptor.getValue().getType());
// Set up watch device as only available BT device.
Collection<BluetoothDevice> availableDevices = Collections.singleton(mockWatchDevice);
@@ -872,13 +1222,94 @@
// available route.
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
null, availableDevices);
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
}
+ @SmallTest
+ @Test
+ public void testQuiescentBluetoothRouteResetMute() throws Exception {
+ when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true);
+ when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ CallAudioState initState = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+ stateMachine.initialize(initState);
+
+ // Switch to active and mute
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ assertTrue(stateMachine.isInActiveState());
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.MUTE_ON);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ CallAudioState expectedState = new CallAudioState(true,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ when(mockAudioManager.isMicrophoneMute()).thenReturn(true);
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.NO_FOCUS);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ expectedState = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ verify(mockAudioService).setMicrophoneMute(eq(false), anyString(), anyInt(), eq(null));
+ }
+
+ @SmallTest
+ @Test
+ public void testSupportRouteMaskUpdateWhenBtAudioConnected() {
+ when(mFeatureFlags.updateRouteMaskWhenBtConnected()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ CallAudioState initState = new CallAudioState(false,
+ CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ stateMachine.initialize(initState);
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
private void initializationTestHelper(CallAudioState expectedState,
int earpieceControl) {
when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(
@@ -897,7 +1328,9 @@
mAudioServiceFactory,
earpieceControl,
mThreadHandler.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.initialize();
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
}
@@ -937,4 +1370,59 @@
doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
any(CallAudioState.class));
}
+
+ private void verifySetAndClearHeadsetCommunicationDevice(int audioType) {
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ AudioDeviceInfo headset = mock(AudioDeviceInfo.class);
+ when(headset.getType()).thenReturn(audioType);
+ when(headset.getAddress()).thenReturn("");
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(headset);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(eq(headset)))
+ .thenReturn(true);
+ when(mockAudioManager.getCommunicationDevice()).thenReturn(headset);
+
+ CallAudioState initState = new CallAudioState(false,
+ CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_EARPIECE);
+ stateMachine.initialize(initState);
+
+ // Switch to active
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ // Make sure that we've successfully switched to the active headset and that we set the
+ // communication device.
+ assertTrue(stateMachine.isInActiveState());
+ ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+ AudioDeviceInfo.class);
+ verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+ assertEquals(audioType, infoArgumentCaptor.getValue().getType());
+
+ // Route out of headset route
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ // Assert that communication device was cleared
+ verify(mockAudioManager).clearCommunicationDevice();
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
index cf684de..6b9b5c8 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
@@ -20,10 +20,10 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
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.timeout;
import static org.mockito.Mockito.verify;
@@ -37,9 +37,11 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.telecom.CallAudioState;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.CallsManager;
@@ -155,6 +157,7 @@
@Mock StatusBarNotifier mockStatusBarNotifier;
@Mock Call fakeCall;
@Mock CallAudioManager mockCallAudioManager;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
private static final int TEST_TIMEOUT = 500;
private AudioManager mockAudioManager;
@@ -174,6 +177,8 @@
mHandlerThread.start();
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+ mCommunicationDeviceTracker.setBluetoothRouteManager(mockBluetoothRouteManager);
mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
@Override
@@ -270,7 +275,9 @@
mAudioServiceFactory,
mParams.earpieceControl,
mHandlerThread.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
setupMocksForParams(stateMachine, mParams);
@@ -288,17 +295,17 @@
if (mParams.initialRoute == CallAudioState.ROUTE_BLUETOOTH) {
stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
}
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
// Clear invocations on mocks to discard stuff from initialization
clearInvocations();
sendActionToStateMachine(stateMachine);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
- Handler h = stateMachine.getHandler();
+ Handler h = stateMachine.getAdapterHandler();
waitForHandlerAction(h, TEST_TIMEOUT);
stateMachine.quitStateMachine();
@@ -311,7 +318,7 @@
break;
case ON:
if (mParams.expectedBluetoothDevice == null) {
- verify(mockBluetoothRouteManager).connectBluetoothAudio(null);
+ verify(mockBluetoothRouteManager, atLeastOnce()).connectBluetoothAudio(null);
} else {
verify(mockBluetoothRouteManager).connectBluetoothAudio(
mParams.expectedBluetoothDevice.getAddress());
@@ -367,7 +374,9 @@
mAudioServiceFactory,
mParams.earpieceControl,
mHandlerThread.getLooper(),
- Runnable::run /** do async stuff sync for test purposes */);
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
stateMachine.setCallAudioManager(mockCallAudioManager);
// Set up bluetooth and speakerphone state
@@ -388,8 +397,8 @@
// Omit the focus-getting statement
sendActionToStateMachine(stateMachine);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
stateMachine.quitStateMachine();
diff --git a/tests/src/com/android/server/telecom/tests/CallControlTest.java b/tests/src/com/android/server/telecom/tests/CallControlTest.java
index 2613206..c69521a 100644
--- a/tests/src/com/android/server/telecom/tests/CallControlTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallControlTest.java
@@ -39,14 +39,7 @@
import java.util.UUID;
public class CallControlTest extends TelecomTestCase {
-
- private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
- new ComponentName("foo", "bar"), "1");
-
- @Mock
- private ICallControl mICallControl;
- @Mock
- private ClientTransactionalServiceRepository mRepository;
+ @Mock private ICallControl mICallControl;
private static final String CALL_ID_1 = UUID.randomUUID().toString();
@Override
@@ -64,15 +57,7 @@
@Test
public void testGetCallId() {
- CallControl control = new CallControl(CALL_ID_1, mICallControl, mRepository, mHandle);
+ CallControl control = new CallControl(CALL_ID_1, mICallControl);
assertEquals(CALL_ID_1, control.getCallId().toString());
}
-
- @Test
- public void testCallControlHitsIllegalStateException() {
- CallControl control = new CallControl(CALL_ID_1, null, mRepository, mHandle);
- assertThrows(IllegalStateException.class, () ->
- control.setInactive(Runnable::run, result -> {
- }));
- }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
index f4008aa..b8b9560 100644
--- a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.tests;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -39,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;
@@ -50,7 +52,9 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
@RunWith(JUnit4.class)
public class CallEndpointControllerTest extends TelecomTestCase {
@@ -81,6 +85,9 @@
availableBluetooth1);
private static final CallAudioState audioState7 = new CallAudioState(false,
CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+ private static final CallAudioState audioState8 = new CallAudioState(false,
+ CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+ availableBluetooth2);
private CallEndpointController mCallEndpointController;
@@ -95,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();
@@ -177,6 +187,74 @@
verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
}
+ /**
+ * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user
+ * requests to switch to a bluetooth CallEndpoint. This is an edge case where bluetooth is not
+ * the current CallEndpoint but the CallAudioState shows the bluetooth device is
+ * active/available.
+ */
+ @Test
+ public void testSwitchFromEarpieceToBluetooth() {
+ // simulate an audio state where the EARPIECE is active but a bluetooth device is active.
+ mCallEndpointController.onCallAudioStateChanged(null, audioState8 /* Ear but BT active */);
+ CallEndpoint btEndpoint = mCallEndpointController.getAvailableEndpoints().stream()
+ .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH)
+ .toList().get(0); // get the only available BT endpoint
+
+ // verify the CallEndpointController shows EARPIECE active + BT endpoint is active device
+ assertEquals(CallEndpoint.TYPE_EARPIECE,
+ mCallEndpointController.getCurrentCallEndpoint().getEndpointType());
+ assertNotNull(btEndpoint);
+
+ // request an endpoint change from earpiece to the bluetooth
+ doReturn(audioState8).when(mCallAudioManager).getCallAudioState();
+ mCallEndpointController.requestCallEndpointChange(btEndpoint, mResultReceiver);
+
+ // verify the transaction was successful and CallAudioManager#setAudioRoute was called
+ verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+ verify(mCallAudioManager, times(1)).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+ eq(bluetoothDevice1.getAddress()));
+ }
+
+
+ /**
+ * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user
+ * requests to switch to from one bluetooth device to another.
+ */
+ @Test
+ public void testBtDeviceSwitch() {
+ // bluetoothDevice1 should start as active and bluetoothDevice2 is available
+ mCallEndpointController.onCallAudioStateChanged(null, audioState2 /* BT active D1 */);
+ CallEndpoint currentEndpoint = mCallEndpointController.getCurrentCallEndpoint();
+ List<CallEndpoint> btEndpoints = mCallEndpointController.getAvailableEndpoints().stream()
+ .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH)
+ .toList(); // get the only available BT endpoint
+
+ // verify the initial state of the test
+ assertEquals(2, btEndpoints.size());
+ assertEquals(CallEndpoint.TYPE_BLUETOOTH, currentEndpoint.getEndpointType());
+
+ CallEndpoint otherBluetoothEndpoint = null;
+ for (CallEndpoint e : btEndpoints) {
+ if (!e.equals(currentEndpoint)) {
+ otherBluetoothEndpoint = e;
+ }
+ }
+
+ assertNotNull(otherBluetoothEndpoint);
+ assertNotEquals(currentEndpoint, otherBluetoothEndpoint);
+
+ // request an endpoint change from BT D1 --> BT D2
+ doReturn(audioState2).when(mCallAudioManager).getCallAudioState();
+ mCallEndpointController.requestCallEndpointChange(otherBluetoothEndpoint, mResultReceiver);
+
+ // verify the transaction was successful and CallAudioManager#setAudioRoute was called
+ verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+ verify(mCallAudioManager, times(1))
+ .setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+ eq(bluetoothDevice2.getAddress()));
+ }
+
@Test
public void testAvailableEndpointChanged() throws Exception {
mCallEndpointController.onCallAudioStateChanged(audioState1, audioState6);
diff --git a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
index cf44cfe..be8e6fb 100644
--- a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
@@ -27,10 +27,10 @@
import android.telecom.Connection;
import android.telecom.InCallService;
import android.telecom.ParcelableCall;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
import androidx.test.filters.FlakyTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
import org.junit.After;
import org.junit.Before;
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 c68cbbf..cb04dc3 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -16,12 +16,15 @@
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.assertNotNull;
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;
@@ -36,6 +39,7 @@
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.location.Country;
@@ -44,6 +48,7 @@
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.SystemClock;
@@ -59,10 +64,10 @@
import android.telecom.VideoProfile;
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Analytics;
import com.android.server.telecom.AnomalyReporterAdapter;
@@ -73,6 +78,7 @@
import com.android.server.telecom.MissedCallNotifier;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.TelephonyUtil;
+import com.android.server.telecom.flags.FeatureFlags;
import org.junit.After;
import org.junit.Before;
@@ -86,6 +92,9 @@
import org.mockito.stubbing.Answer;
import java.util.Arrays;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
@RunWith(JUnit4.class)
public class CallLogManagerTest extends TelecomTestCase {
@@ -114,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";
@@ -127,13 +137,16 @@
@Mock
AnomalyReporterAdapter mAnomalyReporterAdapter;
+ @Mock
+ FeatureFlags mFeatureFlags;
+
@Override
@Before
public void setUp() throws Exception {
super.setUp();
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
mCallLogManager = new CallLogManager(mContext, mMockPhoneAccountRegistrar,
- mMissedCallNotifier, mAnomalyReporterAdapter);
+ mMissedCallNotifier, mAnomalyReporterAdapter, mFeatureFlags);
mDefaultAccountHandle = new PhoneAccountHandle(
new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
TEST_PHONE_ACCOUNT_ID,
@@ -164,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
@@ -177,13 +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
@@ -218,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
@@ -321,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
@@ -346,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
@@ -360,6 +417,7 @@
VIA_NUMBER_STRING, // viaNumber
null
);
+ when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true);
mCallLogManager.onCallStateChanged(fakeIncomingCall, CallState.ACTIVE,
CallState.DISCONNECTED);
ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
@@ -369,9 +427,9 @@
@MediumTest
@Test
- public void testLogCallDirectionMissed() {
+ 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
@@ -385,6 +443,7 @@
VIA_NUMBER_STRING, // viaNumber
null
);
+ when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(false);
mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
CallState.DISCONNECTED);
@@ -393,14 +452,46 @@
Integer.valueOf(CallLog.Calls.MISSED_TYPE));
// Timeout needed because showMissedCallNotification is called from onPostExecute.
verify(mMissedCallNotifier, timeout(TEST_TIMEOUT_MILLIS))
- .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class));
+ .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class),
+ /* uri= */ eq(null));
+ }
+
+ @MediumTest
+ @Test
+ public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOn() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
+ Call fakeMissedCall = makeFakeCall(
+ DisconnectCause.MISSED, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ null
+ );
+ when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true);
+
+ mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.MISSED_TYPE));
+ // Timeout needed because showMissedCallNotification is called from onPostExecute.
+ verify(mMissedCallNotifier, timeout(TEST_TIMEOUT_MILLIS))
+ .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class),
+ /* uri= */ any(Uri.class));
}
@MediumTest
@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
@@ -426,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(
@@ -454,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
@@ -478,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
@@ -505,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
@@ -563,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));
@@ -608,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));
@@ -618,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));
@@ -646,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() {
@@ -671,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));
@@ -701,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));
@@ -713,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
@@ -740,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
@@ -791,19 +954,34 @@
assertEquals(1, insertedValues.getAsInteger(Calls.IS_READ).intValue());
}
- @SmallTest
@Test
- public void testCountryIso_setCache() {
- Country testCountry = new Country(TEST_ISO, Country.COUNTRY_SOURCE_LOCALE);
- CountryDetector mockDetector = (CountryDetector) mContext.getSystemService(
- Context.COUNTRY_DETECTOR);
- when(mockDetector.detectCountry()).thenReturn(testCountry);
+ public void testLogCallWhenExternalCallOnWatch() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
+ PackageManager packageManager = mContext.getPackageManager();
+ when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+ when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true);
+ Call fakeMissedCall = makeFakeCall(
+ DisconnectCause.REJECTED, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ null
+ );
+ when(fakeMissedCall.isExternalCall()).thenReturn(true);
- String resultIso = mCallLogManager.getCountryIso();
-
- verifyCountryIso(mockDetector, resultIso);
+ mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+ verifyInsertionWithCapture(CURRENT_USER_ID);
}
+
@SmallTest
@Test
public void testCountryIso_newCountryDetected() {
@@ -811,17 +989,28 @@
Country testCountry2 = new Country(TEST_ISO_2, Country.COUNTRY_SOURCE_LOCALE);
CountryDetector mockDetector = (CountryDetector) mContext.getSystemService(
Context.COUNTRY_DETECTOR);
- when(mockDetector.detectCountry()).thenReturn(testCountry);
- // Put TEST_ISO in the Cache
+ Handler handler = new Handler(Looper.getMainLooper());
+
+ String initialIso = mCallLogManager.getCountryIso();
+ assertEquals(Locale.getDefault().getCountry(), initialIso);
+
+ ArgumentCaptor<Consumer<Country>> capture = ArgumentCaptor.forClass(Consumer.class);
+ verify(mockDetector).registerCountryDetectorCallback(
+ any(Executor.class), capture.capture());
+ Consumer<Country> countryConsumer = capture.getValue();
+
+ countryConsumer.accept(testCountry);
+ waitForHandlerAction(handler, TEST_TIMEOUT);
String resultIso = mCallLogManager.getCountryIso();
- ArgumentCaptor<CountryListener> captor = verifyCountryIso(mockDetector, resultIso);
+ assertEquals(TEST_ISO, resultIso);
- // Change ISO to TEST_ISO_2
- CountryListener listener = captor.getValue();
- listener.onCountryDetected(testCountry2);
-
- String resultIso2 = mCallLogManager.getCountryIso();
- assertEquals(TEST_ISO_2, resultIso2);
+ // If default locale is equal to TEST_ISO, test another ISO to assure working functionality.
+ if (initialIso.equals(TEST_ISO)) {
+ countryConsumer.accept(testCountry2);
+ waitForHandlerAction(handler, TEST_TIMEOUT);
+ resultIso = mCallLogManager.getCountryIso();
+ assertEquals(TEST_ISO_2, resultIso);
+ }
}
@SmallTest
@@ -896,6 +1085,56 @@
@SmallTest
@Test
+ public void testDoNotLogCallExtra() {
+ when(mFeatureFlags.telecomSkipLogBasedOnExtra()).thenReturn(true);
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.LOCAL, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ Bundle extras = new Bundle();
+ extras.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+ when(fakeCall.getExtras()).thenReturn(extras);
+
+ assertFalse(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
+ false /* isCanceled */));
+ }
+
+ @SmallTest
+ @Test
+ public void testIgnoresDoNotLogCallExtra_whenFlagDisabled() {
+ when(mFeatureFlags.telecomSkipLogBasedOnExtra()).thenReturn(false);
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.LOCAL, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ VIA_NUMBER_STRING, // viaNumber
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ Bundle extras = new Bundle();
+ extras.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+ when(fakeCall.getExtras()).thenReturn(extras);
+
+ assertTrue(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
+ false /* isCanceled */));
+ }
+
+ @SmallTest
+ @Test
public void testDoNotLogConferenceWithChildren() {
Call fakeCall = makeFakeCall(
DisconnectCause.LOCAL, // disconnectCauseCode
diff --git a/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
index b5c6468..5ccb2fe 100644
--- a/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
@@ -28,7 +28,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -41,11 +40,12 @@
import android.media.AudioRecordingConfiguration;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
-import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.filters.MediumTest;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.server.telecom.Call;
@@ -53,6 +53,7 @@
import com.android.server.telecom.CallState;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.Flags;
import org.junit.After;
import org.junit.Before;
@@ -72,6 +73,7 @@
* Unit tests for the {@link com.android.server.telecom.CallRecordingTonePlayer} class.
*/
@RunWith(JUnit4.class)
+@RequiresFlagsDisabled(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
public class CallRecordingTonePlayerTest extends TelecomTestCase {
private static final String PHONE_ACCOUNT_PACKAGE = "com.android.telecom.test";
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 01446d1..241216a 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -55,10 +55,10 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.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.times;
@@ -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/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index 4d8d497..d97263d 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.tests;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -43,7 +44,8 @@
import android.telecom.ParcelableCall;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.internal.telecom.ICallScreeningAdapter;
import com.android.internal.telecom.ICallScreeningService;
@@ -135,7 +137,7 @@
when(mContext.getSystemService(TelecomManager.class))
.thenReturn(mTelecomManager);
when(mTelecomManager.getSystemDialerPackage()).thenReturn(PKG_NAME);
- when(mAppLabelProxy.getAppLabel(PKG_NAME)).thenReturn(APP_NAME);
+ when(mAppLabelProxy.getAppLabel(PKG_NAME, PA_HANDLE.getUserHandle())).thenReturn(APP_NAME);
when(mParcelableCallUtilsConverter.toParcelableCall(
eq(mCall), anyBoolean(), eq(mPhoneAccountRegistrar))).thenReturn(null);
when(mContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class),
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 997e7dd..3a7a822 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -21,9 +21,8 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
@@ -31,14 +30,20 @@
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.PersistableBundle;
+import android.os.UserHandle;
import android.telecom.CallAttributes;
+import android.telecom.CallEndpoint;
import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -50,11 +55,14 @@
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.CallQuality;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.widget.Toast;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import com.android.server.telecom.CachedAvailableEndpointsChange;
+import com.android.server.telecom.CachedCallEventQueue;
+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;
@@ -62,6 +70,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;
@@ -77,6 +86,7 @@
import org.mockito.Mockito;
import java.util.Collections;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
public class CallTest extends TelecomTestCase {
@@ -99,7 +109,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;
@@ -116,7 +125,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.getString(R.string.skip_incoming_caller_info_account_package))
+ .thenReturn("");
+ EmergencyCallHelper helper = mock(EmergencyCallHelper.class);
+ doReturn(helper).when(mMockCallsManager).getEmergencyCallHelper();
}
@After
@@ -136,6 +152,308 @@
}
/**
+ * Verify that transactional calls remap the [CallAttributes#CallCapability]s to
+ * Connection capabilities.
+ */
+ @Test
+ @SmallTest
+ public void testTransactionalCallCapabilityRemapping() {
+ // ensure when the flag is disabled, the old behavior is unchanged
+ Bundle disabledFlagExtras = new Bundle();
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ disabledFlagExtras.putInt(CallAttributes.CALL_CAPABILITIES_KEY,
+ Connection.CAPABILITY_MERGE_CONFERENCE);
+ when(mFeatureFlags.remapTransactionalCapabilities()).thenReturn(false);
+ call.setTransactionalCapabilities(disabledFlagExtras);
+ assertTrue(call.can(Connection.CAPABILITY_MERGE_CONFERENCE));
+ // enable the bug fix flag and ensure the transactional capabilities are remapped
+ Bundle enabledFlagExtras = new Bundle();
+ Call call2 = createCall("2", Call.CALL_DIRECTION_INCOMING);
+ enabledFlagExtras.putInt(CallAttributes.CALL_CAPABILITIES_KEY,
+ CallAttributes.SUPPORTS_SET_INACTIVE);
+ when(mFeatureFlags.remapTransactionalCapabilities()).thenReturn(true);
+ call2.setTransactionalCapabilities(enabledFlagExtras);
+ assertTrue(call2.can(Connection.CAPABILITY_HOLD));
+ assertTrue(call2.can(Connection.CAPABILITY_SUPPORT_HOLD));
+ }
+
+ /**
+ * 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 testMultipleCachedCallEvents() {
+ when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+ when(mFeatureFlags.cacheCallEvents()).thenReturn(true);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+ assertNull(call.getTransactionServiceWrapper());
+
+ String testEvent1 = "test1";
+ Bundle testBundle1 = new Bundle();
+ testBundle1.putInt("testKey", 1);
+ call.sendCallEvent(testEvent1, testBundle1);
+ assertEquals(1,
+ call.getCachedServiceCallbacksCopy().get(CachedCallEventQueue.ID).size());
+
+ String testEvent2 = "test2";
+ Bundle testBundle2 = new Bundle();
+ testBundle2.putInt("testKey", 2);
+ call.sendCallEvent(testEvent2, testBundle2);
+ assertEquals(2,
+ call.getCachedServiceCallbacksCopy().get(CachedCallEventQueue.ID).size());
+
+ String testEvent3 = "test3";
+ Bundle testBundle3 = new Bundle();
+ testBundle2.putInt("testKey", 3);
+ call.sendCallEvent(testEvent3, testBundle3);
+ assertEquals(3,
+ call.getCachedServiceCallbacksCopy().get(CachedCallEventQueue.ID).size());
+
+ verify(tsw, times(0)).sendCallEvent(any(), any(), any());
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent1), eq(testBundle1));
+ verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent2), eq(testBundle2));
+ verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent3), eq(testBundle3));
+ assertEquals(0, call.getCachedServiceCallbacksCopy().size());
+ }
+
+ @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.getCachedServiceCallbacksCopy().get(CachedMuteStateChange.ID).size());
+
+ call.cacheServiceCallback(new CachedMuteStateChange(false));
+ assertEquals(1,
+ call.getCachedServiceCallbacksCopy().get(CachedMuteStateChange.ID).size());
+
+ CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+ .getCachedServiceCallbacksCopy()
+ .get(CachedMuteStateChange.ID)
+ .getLast();
+
+ assertFalse(currentCacheMuteState.isMuted());
+
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).onMuteStateChanged(any(), eq(false));
+ assertEquals(0, call.getCachedServiceCallbacksCopy().size());
+ }
+
+ @Test
+ public void testCacheAfterServiceSet() {
+ when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+ when(mFeatureFlags.cacheCallEvents()).thenReturn(true);
+ TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+ assertNull(call.getTransactionServiceWrapper());
+ call.setTransactionServiceWrapper(tsw);
+ call.cacheServiceCallback(new CachedMuteStateChange(true));
+ // Ensure that we do not lose events if for some reason a CachedCallback is cached after
+ // the service is set
+ verify(tsw, times(1)).onMuteStateChanged(any(), eq(true));
+ assertEquals(0, call.getCachedServiceCallbacksCopy().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.getCachedServiceCallbacksCopy().get(CachedCurrentEndpointChange.ID).size());
+
+ call.cacheServiceCallback(new CachedCurrentEndpointChange(speaker));
+ assertEquals(1,
+ call.getCachedServiceCallbacksCopy().get(CachedCurrentEndpointChange.ID).size());
+
+ CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+ .getCachedServiceCallbacksCopy()
+ .get(CachedCurrentEndpointChange.ID)
+ .getLast();
+
+ assertEquals(CallEndpoint.TYPE_SPEAKER,
+ currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+ assertEquals(0, call.getCachedServiceCallbacksCopy().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.getCachedServiceCallbacksCopy().get(CachedAvailableEndpointsChange.ID).size());
+
+ call.cacheServiceCallback(new CachedAvailableEndpointsChange(finalSet));
+ assertEquals(1,
+ call.getCachedServiceCallbacksCopy().get(CachedAvailableEndpointsChange.ID).size());
+
+ CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+ .getCachedServiceCallbacksCopy()
+ .get(CachedAvailableEndpointsChange.ID)
+ .getLast();
+
+ assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+ call.setTransactionServiceWrapper(tsw);
+ verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+ assertEquals(0, call.getCachedServiceCallbacksCopy().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);
+ when(mFeatureFlags.cacheCallEvents()).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.getCachedServiceCallbacksCopy().size());
+ call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+ assertEquals(2, call.getCachedServiceCallbacksCopy().size());
+ call.cacheServiceCallback(new CachedAvailableEndpointsChange(availableEndpointsSet));
+ assertEquals(3, call.getCachedServiceCallbacksCopy().size());
+ String testEvent = "testEvent";
+ Bundle testBundle = new Bundle();
+ call.sendCallEvent("testEvent", testBundle);
+
+ // verify the cached callbacks are stored properly within the cache map and the values
+ // can be evaluated
+ CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+ .getCachedServiceCallbacksCopy()
+ .get(CachedMuteStateChange.ID)
+ .getLast();
+ CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+ .getCachedServiceCallbacksCopy()
+ .get(CachedCurrentEndpointChange.ID)
+ .getLast();
+ CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+ .getCachedServiceCallbacksCopy()
+ .get(CachedAvailableEndpointsChange.ID)
+ .getLast();
+ 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());
+ verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent), eq(testBundle));
+
+ // the cache map should be cleared
+ assertEquals(0, call.getCachedServiceCallbacksCopy().size());
+ }
+
+ /**
* Basic tests to check which call states are considered transitory.
*/
@Test
@@ -200,7 +518,8 @@
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
// To start with connection creation isn't complete.
assertFalse(call.isCreateConnectionComplete());
@@ -301,7 +620,6 @@
doReturn(true).when(mMockCallsManager).isInEmergencyCall();
call.pullExternalCall();
verify(mMockConnectionService, never()).pullExternalCall(any());
- verify(mMockToast).show();
}
@Test
@@ -338,7 +656,8 @@
false /* shouldAttachToExistingConnection*/,
true /* isConference */,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
assertFalse(call.wasDndCheckComputedForCall());
assertFalse(call.isCallSuppressedByDoNotDisturb());
@@ -364,7 +683,8 @@
false /* shouldAttachToExistingConnection*/,
true /* isConference */,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
assertNull(call.getConnectionServiceWrapper());
assertFalse(call.isTransactionalCall());
@@ -394,7 +714,8 @@
false /* shouldAttachToExistingConnection*/,
true /* isConference */,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
// setup
call.setIsTransactionalCall(true);
@@ -467,6 +788,18 @@
@Test
@SmallTest
+ public void testGetFromCallerInfo_skipLookup() {
+ Resources mockResources = mContext.getResources();
+ when(mockResources.getString(R.string.skip_incoming_caller_info_account_package))
+ .thenReturn("com.foo");
+
+ createCall("1");
+
+ verify(mMockCallerInfoLookupHelper, never()).startLookup(any(), any());
+ }
+
+ @Test
+ @SmallTest
public void testOriginalCallIntent() {
Call call = createCall("1");
@@ -728,11 +1061,91 @@
}));
}
+ @Test
+ @SmallTest
+ public void testExcludesInCallServiceFromDoNotLogCallExtra() {
+ Call call = createCall("any");
+ Bundle extra = new Bundle();
+ extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+
+ call.putInCallServiceExtras(extra, "packageName");
+
+ assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
+ }
+
+ /**
+ * 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() {
+ PackageManager packageManager = mContext.getPackageManager();
+ Bundle extra = new Bundle();
+ extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+ String packageName = SIM_1_HANDLE.getComponentName().getPackageName();
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(packageManager)
+ .checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, packageName);
+ Call call = createCall("any");
+
+ call.putConnectionServiceExtras(extra);
+
+ assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
+ }
+
+ @Test
+ @SmallTest
+ public void testDoesNotExcludeConnectionServiceWithModifyStatePermissionFromDoNotLogCallExtra() {
+ String packageName = SIM_1_HANDLE.getComponentName().getPackageName();
+ Bundle extra = new Bundle();
+ extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+ PackageManager packageManager = mContext.getPackageManager();
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(packageManager)
+ .checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, packageName);
+ Call call = createCall("any");
+
+ call.putConnectionServiceExtras(extra);
+
+ assertTrue(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
+ }
+
+ @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,
@@ -740,7 +1153,7 @@
mLock,
null,
mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
+ address,
null /* GatewayInfo */,
null,
SIM_1_HANDLE,
@@ -748,6 +1161,7 @@
false,
false,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
}
}
diff --git a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
index 7c001c0..614ef71 100644
--- a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
@@ -17,11 +17,11 @@
package com.android.server.telecom.tests;
import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
+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.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -32,13 +32,13 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.telecom.Logging.Session;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import androidx.test.InstrumentationRegistry;
-
import android.telecom.CallerInfo;
import android.telecom.CallerInfoAsyncQuery;
+import android.telecom.Logging.Session;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
import com.android.server.telecom.CallerInfoAsyncQueryFactory;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.ContactsAsyncHelper;
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 649e54a..79fd3d5 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -33,9 +33,12 @@
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;
import static org.mockito.Mockito.reset;
@@ -43,6 +46,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+
import static java.lang.Thread.sleep;
import android.Manifest;
@@ -63,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;
@@ -77,16 +81,19 @@
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneCapability;
import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
import android.util.Pair;
import android.widget.Toast;
+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;
import com.android.server.telecom.CallAnomalyWatchdog;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioModeStateMachine;
import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -123,15 +130,21 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.TransactionManager;
+
+import com.google.common.base.Objects;
import org.junit.After;
import org.junit.Before;
@@ -139,7 +152,8 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
-import org.mockito.Matchers;
+import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -149,10 +163,12 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
@RunWith(JUnit4.class)
public class CallsManagerTest extends TelecomTestCase {
@@ -168,6 +184,10 @@
new UserHandle(SECONDARY_USER_ID));
private static final PhoneAccountHandle SIM_2_HANDLE = new PhoneAccountHandle(
ComponentName.unflattenFromString("com.foo/.Blah"), "Sim2");
+ private static final PhoneAccountHandle SIM_3_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.foo/.Blah"), "Sim3");
+ private static final PhoneAccountHandle CALL_PROVIDER_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.sip.foo/.Blah"), "sip1");
private static final PhoneAccountHandle CONNECTION_MGR_1_HANDLE = new PhoneAccountHandle(
ComponentName.unflattenFromString("com.bar/.Conn"), "Cm1");
private static final PhoneAccountHandle CONNECTION_MGR_2_HANDLE = new PhoneAccountHandle(
@@ -201,6 +221,18 @@
| PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
.setIsEnabled(true)
.build();
+ private static final PhoneAccount SIM_3_ACCOUNT = new PhoneAccount.Builder(SIM_3_HANDLE, "Sim3")
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+ .setIsEnabled(true)
+ .build();
+ private static final PhoneAccount CALL_PROVIDER_ACCOUNT =
+ new PhoneAccount.Builder(CALL_PROVIDER_HANDLE, "sip1")
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+ .setIsEnabled(true)
+ .build();
private static final PhoneAccount SELF_MANAGED_ACCOUNT = new PhoneAccount.Builder(
SELF_MANAGED_HANDLE, "Self")
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
@@ -272,7 +304,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;
@@ -280,9 +311,16 @@
@Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
@Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
@Mock private PhoneCapability mPhoneCapability;
+ @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Mock private CallStreamingNotification mCallStreamingNotification;
+ @Mock private BluetoothDeviceManager mBluetoothDeviceManager;
+ @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;
-
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
private CallsManager mCallsManager;
@Override
@@ -301,8 +339,8 @@
when(mCallEndpointControllerFactory.create(any(), any(), any())).thenReturn(
mCallEndpointController);
when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(),
- anyInt(), any())).thenReturn(mCallAudioRouteStateMachine);
- when(mCallAudioModeStateMachineFactory.create(any(), any()))
+ anyInt(), any(), any(), any())).thenReturn(mCallAudioRouteStateMachine);
+ when(mCallAudioModeStateMachineFactory.create(any(), any(), any(), any()))
.thenReturn(mCallAudioModeStateMachine);
when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
@@ -355,7 +393,14 @@
mBlockedNumbersAdapter,
TransactionManager.getTestInstance(),
mEmergencyCallDiagnosticLogger,
- mCallStreamingNotification);
+ mCommunicationDeviceTracker,
+ mCallStreamingNotification,
+ mBluetoothDeviceManager,
+ mFeatureFlags,
+ mTelephonyFlags,
+ (call, listener, context, timeoutsAdapter,
+ mFeatureFlags, lock) -> mIncomingCallFilterGraph,
+ mMockTelecomMetricsController);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -364,9 +409,17 @@
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SIM_2_HANDLE), any())).thenReturn(SIM_2_ACCOUNT);
when(mPhoneAccountRegistrar.getPhoneAccount(
+ eq(SIM_3_HANDLE), any())).thenReturn(SIM_3_ACCOUNT);
+ when(mPhoneAccountRegistrar.getPhoneAccount(
+ 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(
@@ -384,8 +437,21 @@
@MediumTest
@Test
public void testConstructPossiblePhoneAccounts() throws Exception {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
+ setupMsimAccounts();
// Should be empty since the URI is null.
- assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null, false, false).size());
+ assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null,
+ false, false, false).size());
+ }
+
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccounts_simulCalling() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupMsimAccounts();
+ // Should be empty since the URI is null.
+ assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null,
+ false, false, false).size());
}
private Call constructOngoingCall(String callId, PhoneAccountHandle phoneAccountHandle) {
@@ -404,10 +470,12 @@
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
ongoingCall.setState(CallState.ACTIVE, "just cuz");
return ongoingCall;
}
+
/**
* Verify behavior for multisim devices where we want to ensure that the active sim is used for
* placing a new call.
@@ -416,32 +484,127 @@
@MediumTest
@Test
public void testConstructPossiblePhoneAccountsMultiSimActive() throws Exception {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
setupMsimAccounts();
Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
mCallsManager.addCall(ongoingCall);
List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
- TEST_ADDRESS, null, false, false);
+ TEST_ADDRESS, null, false, false, false);
assertEquals(1, phoneAccountHandles.size());
assertEquals(SIM_2_HANDLE, phoneAccountHandles.get(0));
}
/**
+ * Verify behavior for multisim devices where we want to ensure that the active sim is used for
+ * placing a new call when a restriction is set as well as other call providers from different
+ * apps.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRestriction() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+ CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false, false);
+ assertEquals(Arrays.asList(SIM_2_HANDLE, CALL_PROVIDER_HANDLE), phoneAccountHandles);
+ }
+
+ /**
+ * When we have 3 SIM PhoneAccounts on a device, but only 2 allow simultaneous calling, place a
+ * call on a SIM that allows simultaneous calling and verify that the subset of PhoneAccounts
+ * are available when in a call as well as the call provider.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRestrictionSubset() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, SIM_3_HANDLE,
+ CALL_PROVIDER_HANDLE), new ArraySet<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false, false);
+ assertEquals(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, CALL_PROVIDER_HANDLE),
+ phoneAccountHandles);
+ }
+
+ /**
+ * When we have 3 SIM PhoneAccounts on a device, but only 2 allow simultaneous calling, place a
+ * call on the SIM that does not allow simultaneous calling and verify that only that SIM and
+ * the separate call provider are allowed to place a second call.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRestrictionSubset2() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, SIM_3_HANDLE,
+ CALL_PROVIDER_HANDLE), new ArraySet<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+
+ Call ongoingCall = constructOngoingCall("1", SIM_3_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false, false);
+ assertEquals(Arrays.asList(SIM_3_HANDLE, CALL_PROVIDER_HANDLE),
+ phoneAccountHandles);
+ }
+
+ /**
* Verify behavior for multisim devices when there are no calls active; expect both accounts.
* @throws Exception
*/
@MediumTest
@Test
public void testConstructPossiblePhoneAccountsMultiSimIdle() throws Exception {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
setupMsimAccounts();
List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
- TEST_ADDRESS, null, false, false);
+ TEST_ADDRESS, null, false, false, false);
assertEquals(2, phoneAccountHandles.size());
}
/**
+ * Verify behavior for multisim devices when there are no calls active and there are no calling
+ * restrictions set; expect both accounts.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimIdle_noSimulCallingRestriction() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsNoSimultaneousCallingRestriction();
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false, false);
+ assertEquals(3, phoneAccountHandles.size());
+ }
+
+ /**
+ * Verify behavior for multisim devices when there are no calls active and there are no calling
+ * restrictions set; expect both accounts.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimIdle_withSimulCallingRestriction() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+ CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false, false);
+ assertEquals(3, phoneAccountHandles.size());
+ }
+
+ /**
* For DSDA-enabled multisim devices with an ongoing call, verify that both SIMs'
* PhoneAccountHandles are constructed while placing a new call.
* @throws Exception
@@ -450,6 +613,7 @@
@Test
public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible() throws
Exception {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
setupMsimAccounts();
setMaxActiveVoiceSubscriptions(2);
@@ -457,11 +621,29 @@
mCallsManager.addCall(ongoingCall);
List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
- TEST_ADDRESS, null, false, false);
+ TEST_ADDRESS, null, false, false, false);
assertEquals(2, phoneAccountHandles.size());
}
/**
+ * For multisim devices with an ongoing call, verify that all call capable PhoneAccounts are
+ * available when creating a second call.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_simulCalling_dsdaPossible() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsNoSimultaneousCallingRestriction();
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false, false);
+ assertEquals(3, phoneAccountHandles.size());
+ }
+
+ /**
* For DSDA-enabled multisim devices with an ongoing call, verify that only the active SIMs'
* PhoneAccountHandle is constructed while placing an emergency call.
* @throws Exception
@@ -470,6 +652,7 @@
@Test
public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible_emergencyCall()
throws Exception {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
setupMsimAccounts();
setMaxActiveVoiceSubscriptions(2);
@@ -477,11 +660,96 @@
mCallsManager.addCall(ongoingCall);
List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
- TEST_ADDRESS, null, false, true /* isEmergency */);
+ TEST_ADDRESS, null, false, true /* isEmergency */, false);
assertEquals(1, phoneAccountHandles.size());
assertEquals(SIM_2_HANDLE, phoneAccountHandles.get(0));
}
+ /**
+ * For multisim devices with an ongoing call, verify that only the active SIM's
+ * PhoneAccountHandle is available if we have a calling restriction where only one SIM is
+ * active at a time.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_simulCalling_emergencyCall() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+ CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, true /* isEmergency */, false);
+ assertEquals(2, phoneAccountHandles.size());
+ assertTrue("Candidate PAHs must contain the SIM account hosting the emergency call",
+ phoneAccountHandles.contains(SIM_2_HANDLE));
+ assertFalse("Candidate PAHs must not contain other SIM accounts",
+ phoneAccountHandles.contains(SIM_1_HANDLE));
+ }
+
+ /**
+ * For devices with an ongoing call on a call provider, verify that when an emergency
+ * call is placed, all SIM accounts are still available for SIM selection.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccounts_callProvider_emergencyCall() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+ CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+ Call ongoingCall = constructOngoingCall("1", CALL_PROVIDER_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, true /* isEmergency */, false);
+ assertEquals(3, phoneAccountHandles.size());
+ }
+
+ /**
+ * For multisim devices with an ongoing call, for backwards compatibility, only allow the
+ * SIM with the active call to be chosen to place an emergency call, even if there is no
+ * calling restriction.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRest_emergencyCall() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsNoSimultaneousCallingRestriction();
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, true /* isEmergency */, false);
+ assertEquals(2, phoneAccountHandles.size());
+ assertTrue("Candidate PAHs must contain the SIM account hosting the emergency call",
+ phoneAccountHandles.contains(SIM_2_HANDLE));
+ assertFalse("Candidate PAHs must not contain other SIM accounts",
+ phoneAccountHandles.contains(SIM_1_HANDLE));
+ }
+
+ /**
+ * For multisim devices with an ongoing call on a call provider, it is still possible to place
+ * a SIM call on any SIM account.
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccounts_crossAccount_simulCalling() {
+ when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+ setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+ CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+ Call ongoingCall = constructOngoingCall("1", CALL_PROVIDER_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false /* isEmergency */, false);
+ assertEquals(3, phoneAccountHandles.size());
+ }
+
private void setupCallerInfoLookupHelper() {
doAnswer(invocation -> {
Uri handle = invocation.getArgument(0);
@@ -1188,6 +1456,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() {
@@ -1331,8 +1629,9 @@
@SmallTest
@Test
- public void testNoFilteringOfCallsWhenPhoneAccountRequestsSkipped() {
+ public void testDndFilterAppliesOfCallsWhenPhoneAccountRequestsSkipped() {
// GIVEN an incoming call which is from a PhoneAccount that requested to skip filtering.
+ when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(true);
Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
Bundle extras = new Bundle();
extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
@@ -1352,7 +1651,35 @@
// WHEN the incoming call is successfully added.
mCallsManager.onSuccessfulIncomingCall(incomingCall);
- // THEN the incoming call is not using call filtering
+ // THEN the incoming call is still applying Dnd filter.
+ verify(incomingCall).setIsUsingCallFiltering(eq(true));
+ }
+
+ @SmallTest
+ @Test
+ public void testNoFilterAppliesOfCallsWhenFlagNotEnabled() {
+ // Flag is not enabled.
+ when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(false);
+ Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
+ Bundle extras = new Bundle();
+ extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
+ PhoneAccount skipRequestedAccount = new PhoneAccount.Builder(SIM_2_HANDLE, "Skipper")
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setExtras(extras)
+ .setIsEnabled(true)
+ .build();
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE))
+ .thenReturn(skipRequestedAccount);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(false).when(incomingCall).isSelfManaged();
+ doReturn(true).when(incomingCall).setState(anyInt(), any());
+
+ // WHEN the incoming call is successfully added.
+ mCallsManager.onSuccessfulIncomingCall(incomingCall);
+
+ // THEN the incoming call is not applying filter.
verify(incomingCall).setIsUsingCallFiltering(eq(false));
}
@@ -2409,7 +2736,7 @@
mCallsManager.onCallFilteringComplete(callSpy, result, false /* timeout */);
verify(mMissedCallNotifier).showMissedCallNotification(
- any(MissedCallNotifier.CallInfo.class));
+ any(MissedCallNotifier.CallInfo.class), /* uri= */ eq(null));
}
@Test
@@ -2446,6 +2773,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.
@@ -2513,6 +2858,32 @@
assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName());
}
+ /**
+ * Verify the only call state set from calling onSuccessfulOutgoingCall is CallState.DIALING.
+ */
+ @SmallTest
+ @Test
+ public void testOutgoingCallStateIsSetToAPreviousStateAndIgnored() {
+ when(mFeatureFlags.fixAudioFlickerForOutgoingCalls()).thenReturn(true);
+ Call outgoingCall = addSpyCall(CallState.CONNECTING);
+ mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.NEW);
+ verify(outgoingCall, never()).setState(eq(CallState.NEW), any());
+ verify(outgoingCall, times(1)).setState(eq(CallState.DIALING), any());
+ }
+
+ /**
+ * Verify a ConnectionService can start the call in the active state and avoid the dialing state
+ */
+ @SmallTest
+ @Test
+ public void testOutgoingCallStateCanAvoidDialingAndGoStraightToActive() {
+ when(mFeatureFlags.fixAudioFlickerForOutgoingCalls()).thenReturn(true);
+ Call outgoingCall = addSpyCall(CallState.CONNECTING);
+ mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.ACTIVE);
+ verify(outgoingCall, never()).setState(eq(CallState.DIALING), any());
+ verify(outgoingCall, times(1)).setState(eq(CallState.ACTIVE), any());
+ }
+
@SmallTest
@Test
public void testRejectIncomingCallOnPAHInactive_SecondaryUser() throws Exception {
@@ -2521,11 +2892,9 @@
mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
WORK_HANDLE.getUserHandle(), service);
- UserManager um = mContext.getSystemService(UserManager.class);
- UserHandle newUser = new UserHandle(11);
- when(mCallsManager.getCurrentUserHandle()).thenReturn(newUser);
- when(um.isUserAdmin(eq(newUser.getIdentifier()))).thenReturn(false);
- when(um.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(
@@ -2540,14 +2909,17 @@
@Test
public void testRejectIncomingCallOnPAHInactive_ProfilePaused() throws Exception {
ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
- doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
- mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
- SIM_2_HANDLE.getUserHandle(), service);
+ doReturn(WORK_HANDLE.getComponentName()).when(service).getComponentName();
+ mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
+ WORK_HANDLE.getUserHandle(), service);
- UserManager um = mContext.getSystemService(UserManager.class);
- when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ when(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(
- SIM_2_HANDLE, new Bundle(), false);
+ WORK_HANDLE, new Bundle(), false);
verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any());
assertFalse(newCall.isInECBM());
@@ -2564,8 +2936,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);
@@ -2583,11 +2955,9 @@
when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(WORK_HANDLE)))
.thenReturn(true);
- UserManager um = mContext.getSystemService(UserManager.class);
- UserHandle newUser = new UserHandle(11);
- when(mCallsManager.getCurrentUserHandle()).thenReturn(newUser);
- when(um.isUserAdmin(eq(newUser.getIdentifier()))).thenReturn(false);
- when(um.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(
@@ -2608,8 +2978,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);
@@ -2672,42 +3042,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
@@ -2749,8 +3229,9 @@
public void testQueryCurrentLocationCheckOnReceiveResult() throws Exception {
ConnectionServiceWrapper service = new ConnectionServiceWrapper(
new ComponentName(mContext.getPackageName(),
- mContext.getPackageName().getClass().getName()),
- null, mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null);
+ mContext.getPackageName().getClass().getName()), null,
+ mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null,
+ mFeatureFlags);
CompletableFuture<String> resultFuture = new CompletableFuture<>();
try {
@@ -2774,7 +3255,7 @@
public void testConnectionServiceCreateConnectionTimeout() throws Exception {
ConnectionServiceWrapper service = new ConnectionServiceWrapper(
SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
- mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null);
+ mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
service.setScheduledExecutorService(scheduledExecutorService);
Call call = addSpyCall();
@@ -2848,10 +3329,10 @@
Bundle extras = mock(Bundle.class);
when(call.getIntentExtras()).thenReturn(extras);
- final int attachmentDisabledMask = ~0
- ^ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_LOCATION
- ^ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT
- ^ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY;
+ final int attachmentDisabledMask = ~(
+ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_LOCATION |
+ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT |
+ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY);
CallScreeningService.ParcelableCallResponse response =
mock(CallScreeningService.ParcelableCallResponse.class);
when(response.getCallComposerAttachmentsToShow()).thenReturn(attachmentDisabledMask);
@@ -3008,11 +3489,9 @@
mCallsManager.createActionSetCallStateAndPerformAction(
call, CallState.DISCONNECTED, "");
- verify(sourceCall).onConnectionEvent(eq(Connection.EVENT_HANDOVER_FAILED), any());
verify(sourceCall).onHandoverFailed(
android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
- verify(call).sendCallEvent(eq(android.telecom.Call.EVENT_HANDOVER_FAILED), any());
verify(call).markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_FAILED);
}
@@ -3023,12 +3502,8 @@
Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
when(call.getHandoverDestinationCall()).thenReturn(destinationCall);
when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_FROM_STARTED);
-
mCallsManager.createActionSetCallStateAndPerformAction(
call, CallState.DISCONNECTED, "");
-
- verify(destinationCall).sendCallEvent(
- eq(android.telecom.Call.EVENT_HANDOVER_SOURCE_DISCONNECTED), any());
}
@SmallTest
@@ -3042,11 +3517,8 @@
mCallsManager.createActionSetCallStateAndPerformAction(
call, CallState.DISCONNECTED, "");
- verify(call).onConnectionEvent(eq(Connection.EVENT_HANDOVER_COMPLETE), any());
verify(call).onHandoverComplete();
verify(call).markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
- verify(destinationCall).sendCallEvent(
- eq(android.telecom.Call.EVENT_HANDOVER_COMPLETE), any());
verify(destinationCall).onHandoverComplete();
}
@@ -3063,11 +3535,8 @@
mCallsManager.createActionSetCallStateAndPerformAction(
call, CallState.DISCONNECTED, "");
- verify(call).onConnectionEvent(eq(Connection.EVENT_HANDOVER_COMPLETE), any());
verify(call).onHandoverComplete();
verify(call).markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
- verify(destinationCall).sendCallEvent(
- eq(android.telecom.Call.EVENT_HANDOVER_COMPLETE), any());
verify(destinationCall).onHandoverComplete();
verify(otherCall).disconnect();
}
@@ -3171,18 +3640,14 @@
when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
.thenReturn(true);
mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
- new Intent(
- BlockedNumberContract.SystemContract
- .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.SystemContract
- .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+ new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
eq(false));
}
@@ -3201,9 +3666,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);
@@ -3292,6 +3755,26 @@
assertTrue(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
}
+ @SmallTest
+ @Test
+ public void testBindToBtServiceSeparately() {
+ when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
+ Call call = addSpyCall(CallState.NEW);
+ CallFilteringResult result = new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldReject(false)
+ .build();
+ when(mInCallController.isBoundAndConnectedToBTService(any(UserHandle.class)))
+ .thenReturn(false);
+
+ mCallsManager.onCallFilteringComplete(call, result, false);
+
+ InOrder inOrder = inOrder(mInCallController, call, mInCallController);
+
+ inOrder.verify(mInCallController).bindToBTService(eq(call), eq(null));
+ inOrder.verify(call).setState(eq(CallState.RINGING), anyString());
+ }
+
private Call addSpyCall() {
return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
@@ -3322,8 +3805,8 @@
// Mocks some methods to not call the real method.
doNothing().when(callSpy).unhold();
doNothing().when(callSpy).hold();
- doNothing().when(callSpy).answer(Matchers.anyInt());
- doNothing().when(callSpy).setStartWithSpeakerphoneOn(Matchers.anyBoolean());
+ doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+ doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
mCallsManager.addCall(callSpy);
return callSpy;
@@ -3337,8 +3820,8 @@
doNothing().when(callSpy).unhold();
doNothing().when(callSpy).hold();
doNothing().when(callSpy).disconnect();
- doNothing().when(callSpy).answer(Matchers.anyInt());
- doNothing().when(callSpy).setStartWithSpeakerphoneOn(Matchers.anyBoolean());
+ doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+ doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
return callSpy;
}
@@ -3363,7 +3846,8 @@
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
ongoingCall.setState(initialState, "just cuz");
if (targetPhoneAccount == SELF_MANAGED_HANDLE
|| targetPhoneAccount == SELF_MANAGED_2_HANDLE) {
@@ -3380,6 +3864,9 @@
callback.onRequestFocusDone(call);
}
+ /**
+ * Set up 2 SIM accounts in DSDS mode, where only one SIM can be active at a time for calls.
+ */
private void setupMsimAccounts() {
TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
when(mockTelephonyManager.getMaxNumberOfSimultaneouslyActiveSims()).thenReturn(1);
@@ -3390,12 +3877,76 @@
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
}
+ /**
+ * Set up 2 SIM accounts in DSDS mode and one call provider, where there is no restriction on
+ * simultaneous calls across accounts.
+ */
+ private void setupAccountsNoSimultaneousCallingRestriction() {
+ when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
+ new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, CALL_PROVIDER_HANDLE)));
+ when(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser()).thenReturn(
+ new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+ }
+
+ /**
+ * Set up the call capable PhoneAccounts passed in here to have simultaneous calling
+ * restrictions. If the callCapableHandle is a SIM account and it is in the restriction set, we
+ * will set that callCapableHandle's restriction to the Set. If not, we will set the restriction
+ * to allow no other simultaneous calls.
+ */
+ private void setupAccountsWithCallingRestriction(List<PhoneAccountHandle> callCapableHandles,
+ Set<PhoneAccountHandle> restrictions) {
+ when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
+ new ArrayList<>(callCapableHandles));
+ List<PhoneAccountHandle> allSims = Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+ SIM_3_HANDLE);
+ List<PhoneAccountHandle> simsToTest = callCapableHandles.stream()
+ .filter(allSims::contains).toList();
+ when(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser()).thenReturn(simsToTest);
+ // Remap the PhoneAccounts to inherit the restriction set
+ for (PhoneAccountHandle callCapableHandle : callCapableHandles) {
+ PhoneAccount pa = mPhoneAccountRegistrar.getPhoneAccount(
+ callCapableHandle, callCapableHandle.getUserHandle());
+ assertNotNull("test setup error: could not find PA for PAH:" + callCapableHandle,
+ pa);
+ // For simplicity, for testing only apply restrictions to SIM accounts
+ if (!allSims.contains(callCapableHandle)) continue;
+ if (restrictions.contains(callCapableHandle)) {
+ pa = new PhoneAccount.Builder(pa)
+ .setSimultaneousCallingRestriction(restrictions).build();
+ } else {
+ pa = new PhoneAccount.Builder(pa)
+ .setSimultaneousCallingRestriction(Collections.emptySet()).build();
+ }
+ when(mPhoneAccountRegistrar.getPhoneAccount(eq(callCapableHandle),
+ any())).thenReturn(pa);
+ }
+ }
+
private void setMaxActiveVoiceSubscriptions(int num) {
TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
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();
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index df855e9..1432834 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -16,6 +16,7 @@
package com.android.server.telecom.tests;
+import com.android.server.telecom.flags.FeatureFlags;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
@@ -27,8 +28,11 @@
import org.mockito.stubbing.Answer;
import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.app.AppOpsManager;
import android.app.NotificationManager;
+import android.app.StatsManager;
import android.app.StatusBarManager;
import android.app.UiModeManager;
import android.app.role.RoleManager;
@@ -55,6 +59,7 @@
import android.location.LocationManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.net.Uri;
import android.os.BugreportManager;
import android.os.Bundle;
import android.os.DropBoxManager;
@@ -69,6 +74,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;
@@ -81,8 +87,10 @@
import android.util.DisplayMetrics;
import android.view.accessibility.AccessibilityManager;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -96,7 +104,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
@@ -115,6 +123,7 @@
*/
public class ComponentContextFixture implements TestFixture<Context> {
private HandlerThread mHandlerThread;
+ private Map<UserHandle, Context> mContextsByUser = new HashMap<>();
public class FakeApplicationContext extends MockContext {
@Override
@@ -131,6 +140,9 @@
@Override
public Context createContextAsUser(UserHandle userHandle, int flags) {
+ if (mContextsByUser.containsKey(userHandle)) {
+ return mContextsByUser.get(userHandle);
+ }
return this;
}
@@ -245,6 +257,10 @@
return mSensorPrivacyManager;
case Context.ACCESSIBILITY_SERVICE:
return mAccessibilityManager;
+ case Context.BLOCKED_NUMBERS_SERVICE:
+ return mBlockedNumbersManager;
+ case Context.STATS_MANAGER_SERVICE:
+ return mStatsManager;
default:
return null;
}
@@ -284,6 +300,14 @@
return Context.DROPBOX_SERVICE;
} else if (svcClass == BugreportManager.class) {
return Context.BUGREPORT_SERVICE;
+ } else if (svcClass == TelecomManager.class) {
+ return Context.TELECOM_SERVICE;
+ } else if (svcClass == BlockedNumbersManager.class) {
+ return Context.BLOCKED_NUMBERS_SERVICE;
+ } else if (svcClass == AppOpsManager.class) {
+ return Context.APP_OPS_SERVICE;
+ } else if (svcClass == StatsManager.class) {
+ return Context.STATS_MANAGER_SERVICE;
}
throw new UnsupportedOperationException(svcClass.getName());
}
@@ -409,12 +433,23 @@
}
@Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission) {
+ // Override so that this can be verified via spy.
+ }
+
+ @Override
public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
Bundle options) {
// Override so that this can be verified via spy.
}
@Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
+ int appOp) {
+ // Override so that this can be verified via spy.
+ }
+
+ @Override
public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
int initialCode, String initialData, Bundle initialExtras) {
@@ -614,11 +649,14 @@
private final PermissionInfo mPermissionInfo = mock(PermissionInfo.class);
private final SensorPrivacyManager mSensorPrivacyManager = mock(SensorPrivacyManager.class);
private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
+ private final StatsManager mStatsManager = mock(StatsManager.class);
private TelecomManager mTelecomManager = mock(TelecomManager.class);
+ private BlockedNumbersManager mBlockedNumbersManager = mock(BlockedNumbersManager.class);
- public ComponentContextFixture() {
+ public ComponentContextFixture(FeatureFlags featureFlags) {
MockitoAnnotations.initMocks(this);
+ when(featureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
when(mResources.getConfiguration()).thenReturn(mResourceConfiguration);
when(mResources.getString(anyInt())).thenReturn("");
when(mResources.getStringArray(anyInt())).thenReturn(new String[0]);
@@ -701,7 +739,7 @@
}
}).when(mAppOpsManager).checkPackage(anyInt(), anyString());
- when(mNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
+ when(mNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
when(mCarrierConfigManager.getConfig()).thenReturn(new PersistableBundle());
when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(new PersistableBundle());
@@ -764,6 +802,8 @@
componentName.getPackageName() });
when(mPackageManager.checkPermission(eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mPackageManager.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS),
+ eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
when(mPermissionCheckerManager.checkPermission(
eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
any(AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(),
@@ -802,6 +842,11 @@
when(mResources.getStringArray(eq(id))).thenReturn(value);
}
+ public void putRawResource(int id, String content) {
+ when(mResources.openRawResource(id))
+ .thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
+ }
+
public void setTelecomManager(TelecomManager telecomManager) {
mTelecomManager = telecomManager;
}
@@ -810,6 +855,10 @@
mSubscriptionManager = subscriptionManager;
}
+ public SubscriptionManager getSubscriptionManager() {
+ return mSubscriptionManager;
+ }
+
public TelephonyManager getTelephonyManager() {
return mTelephonyManager;
}
@@ -830,6 +879,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/ConnectionServiceFocusManagerTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
index 0d6ceba..ab2c679 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
@@ -16,7 +16,19 @@
package com.android.server.telecom.tests;
-import android.test.suitebuilder.annotation.SmallTest;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.test.filters.SmallTest;
+
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
@@ -32,17 +44,6 @@
import org.mockito.Mock;
import org.mockito.Mockito;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class ConnectionServiceFocusManagerTest extends TelecomTestCase {
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/ContactsAsyncHelperTest.java b/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
index 10cac93..7adb32c 100644
--- a/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
@@ -17,11 +17,11 @@
package com.android.server.telecom.tests;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyObject;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
@@ -32,11 +32,10 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Handler;
import android.os.Looper;
-import android.test.suitebuilder.annotation.SmallTest;
import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.ContactsAsyncHelper;
@@ -49,7 +48,6 @@
import java.io.FileNotFoundException;
import java.io.InputStream;
-import java.util.concurrent.Executor;
@RunWith(JUnit4.class)
public class ContactsAsyncHelperTest extends TelecomTestCase {
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 0b30656..e497f48 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,53 +16,16 @@
package com.android.server.telecom.tests;
-import android.Manifest;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.UserHandle;
-import android.telecom.DisconnectCause;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import com.android.server.telecom.Call;
-import com.android.server.telecom.CallIdMapper;
-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.PhoneAccountRegistrar;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Random;
-import java.util.UUID;
-
+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.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+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;
@@ -70,15 +33,63 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+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;
+import android.telephony.SubscriptionManager;
+
+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.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;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+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;
+import java.util.Random;
+import java.util.UUID;
+
/**
* Unit testing for CreateConnectionProcessor as well as CreateConnectionTimeout classes.
*/
@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 =
"com.android.server.telecom.tests.MockConnectionService";
+ private static final String CONNECTION_MANAGER_TEST_CLASS =
+ "com.android.server.telecom.tests.ConnectionManagerConnectionService";
private static final UserHandle USER_HANDLE_10 = new UserHandle(10);
@Mock
@@ -91,12 +102,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
@@ -119,11 +132,11 @@
return null;
}
}
- ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
+ ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
mMockConnectionServiceRepository, mMockCreateConnectionResponse,
- mMockAccountRegistrar, mContext);
+ mMockAccountRegistrar, mContext, mFeatureFlags, mTimeoutsAdapter);
mAccountToSub = new HashMap<>();
phoneAccounts = new ArrayList<>();
@@ -146,6 +159,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
@@ -179,7 +197,7 @@
@SmallTest
@Test
- public void testbadPhoneAccount() throws Exception {
+ public void testBadPhoneAccount() throws Exception {
PhoneAccountHandle pAHandle = null;
when(mMockCall.isEmergencyCall()).thenReturn(false);
when(mMockCall.getTargetPhoneAccount()).thenReturn(pAHandle);
@@ -203,13 +221,43 @@
setTargetPhoneAccount(mMockCall, pAHandle);
when(mMockCall.isEmergencyCall()).thenReturn(false);
// Include a Connection Manager
- PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandleForCall(mMockCall,
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
"cm_acct");
+ ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
+ // Make sure the target phone account has the correct permissions
+ PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
+ mFakeTargetPhoneAccount);
- // Need a separate CSW for the connection mgr and the target phone acct.
- ConnectionServiceWrapper targetCsw = configureConnectionServiceWrapper(pAHandle);
- ConnectionServiceWrapper connectionMgrCsw = configureConnectionServiceWrapper(
- callManagerPAHandle);
+ mTestCreateConnectionProcessor.process();
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(eq(callManagerPAHandle));
+ verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall),
+ any(CreateConnectionResponse.class));
+ // Notify successful connection to call
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ @SmallTest
+ @Test
+ public void testConnectionManagerConnectionServiceSuccess() throws Exception {
+ when(mFeatureFlags.updatedRcsCallCountTracking()).thenReturn(true);
+
+ // Configure the target phone account as the remote connection service:
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ setTargetPhoneAccount(mMockCall, pAHandle);
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ ConnectionServiceWrapper remoteService = makeConnectionServiceWrapper();
+
+ // Configure the connection manager phone account as the primary connection service:
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
+ "cm_acct");
+ ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
// Make sure the target phone account has the correct permissions
PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
@@ -221,15 +269,11 @@
verify(mMockCall).setConnectionManagerPhoneAccount(eq(callManagerPAHandle));
verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
- // TODO: This test requires refactoring; it should be targetCsw for the remote CS.
- // However, this test uses phone accounts from all the same component meaning that there
- // is no distinction between the target and connection mgr service. Ideally they should use
- // different packages.
- verify(mMockCall).setConnectionService(eq(connectionMgrCsw) /* primary cs */,
- eq(connectionMgrCsw) /* remote CS */);
- verify(connectionMgrCsw).createConnection(eq(mMockCall),
+ // Ensure the remote connection service and primary connection service are set properly:
+ verify(mMockCall).setConnectionService(eq(service), eq(remoteService));
+ verify(service).createConnection(eq(mMockCall),
any(CreateConnectionResponse.class));
- // Notify successful connection to call
+ // Notify successful connection to call:
CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
@@ -241,10 +285,12 @@
PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
setTargetPhoneAccount(mMockCall, pAHandle);
when(mMockCall.isEmergencyCall()).thenReturn(false);
+ ConnectionServiceWrapper remoteService = makeConnectionServiceWrapper();
+
// Include a Connection Manager
- PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandleForCall(mMockCall,
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
"cm_acct");
- ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
@@ -267,8 +313,8 @@
// Verify that the Sim Phone Account is used correctly
verify(mMockCall).setConnectionManagerPhoneAccount(eq(pAHandle));
verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
- verify(mMockCall).setConnectionService(eq(service));
- verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ verify(mMockCall).setConnectionService(eq(remoteService));
+ verify(remoteService).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
// Notify successful connection to call
CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
@@ -282,7 +328,7 @@
setTargetPhoneAccount(mMockCall, pAHandle);
when(mMockCall.isEmergencyCall()).thenReturn(false);
// Include a Connection Manager
- PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandleForCall(mMockCall,
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
"cm_acct");
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
@@ -312,7 +358,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
@@ -350,7 +397,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).
*/
@@ -380,7 +428,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
@@ -417,7 +466,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.
@@ -457,8 +507,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
@@ -490,7 +542,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
@@ -564,7 +617,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
@@ -677,22 +731,13 @@
// Include a connection Manager for the user with the capability to make calls
PhoneAccount emerCallManagerPA = getNewEmergencyConnectionManagerPhoneAccount("cm_acct",
PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
-
- ConnectionServiceWrapper targetCsw =
- configureConnectionServiceWrapper(regularAccount.getAccountHandle());
- ConnectionServiceWrapper connectionMgrCsw =
- configureConnectionServiceWrapper(callManagerPA.getAccountHandle());
- ConnectionServiceWrapper emergencyConnectionMgrCsw =
- configureConnectionServiceWrapper(emerCallManagerPA.getAccountHandle());
-
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0, null);
phoneAccounts.add(emergencyPhoneAccount);
mapToSubSlot(regularAccount, 2 /*subId*/, 1 /*slotId*/);
mTestCreateConnectionProcessor.process();
reset(mMockCall);
- reset(targetCsw);
- reset(connectionMgrCsw);
- reset(emergencyConnectionMgrCsw);
+ reset(service);
when(mMockCall.getConnectionServiceFocusManager()).thenReturn(
mConnectionServiceFocusManager);
@@ -705,16 +750,14 @@
verify(mMockCall).setConnectionManagerPhoneAccount(
eq(emerCallManagerPA.getAccountHandle()));
verify(mMockCall).setTargetPhoneAccount(eq(regularAccount.getAccountHandle()));
- // Fallback was to the emergency connection mgr, so that CSW should have been set.
- verify(mMockCall).setConnectionService(eq(emergencyConnectionMgrCsw) /* primary */,
- eq(emergencyConnectionMgrCsw) /* remote (ie original target) */);
- verify(emergencyConnectionMgrCsw).createConnection(eq(mMockCall),
- any(CreateConnectionResponse.class));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
}
/**
* 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.
*/
@@ -731,6 +774,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.
@@ -817,8 +1030,8 @@
when(mMockAccountRegistrar.phoneAccountRequiresBindPermission(eq(handle))).thenReturn(true);
}
- private PhoneAccountHandle getNewConnectionMangerHandleForCall(Call call, String id) {
- PhoneAccountHandle callManagerPAHandle = makeQuickAccountHandle(id, null);
+ private PhoneAccountHandle getNewConnectionManagerHandleForCall(Call call, String id) {
+ PhoneAccountHandle callManagerPAHandle = makeQuickConnMgrAccountHandle(id, null);
when(mMockAccountRegistrar.getSimCallManagerFromCall(eq(call))).thenReturn(
callManagerPAHandle);
givePhoneAccountBindPermission(callManagerPAHandle);
@@ -860,14 +1073,37 @@
return new ComponentName(TEST_PACKAGE, TEST_CLASS);
}
+ private static ComponentName makeQuickConnMgrConnectionServiceComponentName() {
+ return new ComponentName(TEST_PACKAGE, CONNECTION_MANAGER_TEST_CLASS);
+ }
+
private ConnectionServiceWrapper makeConnectionServiceWrapper() {
ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
when(mMockConnectionServiceRepository.getService(
- eq(makeQuickConnectionServiceComponentName()),
- any(UserHandle.class))).thenReturn(wrapper);
+ eq(makeQuickConnectionServiceComponentName()), any(UserHandle.class)))
+ .thenReturn(wrapper);
return wrapper;
}
+ private ConnectionServiceWrapper makeConnMgrConnectionServiceWrapper() {
+ ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
+ when(mMockConnectionServiceRepository.getService(
+ eq(makeQuickConnMgrConnectionServiceComponentName()), any(UserHandle.class)))
+ .thenReturn(wrapper);
+ return wrapper;
+ }
+
+ private static PhoneAccountHandle makeQuickConnMgrAccountHandle(String id,
+ UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = Binder.getCallingUserHandle();
+ }
+ return new PhoneAccountHandle(makeQuickConnMgrConnectionServiceComponentName(),
+ id, userHandle);
+ }
+
private static PhoneAccountHandle makeQuickAccountHandle(String id, UserHandle userHandle) {
if (userHandle == null) {
userHandle = Binder.getCallingUserHandle();
@@ -890,18 +1126,5 @@
.setShortDescription("desc" + idx)
.setIsEnabled(true)
.build();
- }
-
- /**
- * Configures a mock ConnectionServiceWrapper for the passed in phone account handle.
- * @param account The phone account handle to use.
- * @return The configured mock.
- */
- private ConnectionServiceWrapper configureConnectionServiceWrapper(PhoneAccountHandle account) {
- ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
- when(mMockConnectionServiceRepository.getService(
- eq(account.getComponentName()),
- any(UserHandle.class))).thenReturn(wrapper);
- return wrapper;
}
}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java b/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
index e733465..3da9284 100644
--- a/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
+++ b/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
@@ -16,6 +16,14 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -24,7 +32,8 @@
import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.DefaultDialerCache;
import com.android.server.telecom.RoleManagerAdapter;
@@ -38,14 +47,6 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class DefaultDialerCacheTest extends TelecomTestCase {
@@ -55,14 +56,17 @@
private static final int USER0 = 0;
private static final int USER1 = 1;
private static final int USER2 = 2;
+ private static final int DELAY_TOLERANCE = 100;
private DefaultDialerCache mDefaultDialerCache;
private ContentObserver mDefaultDialerSettingObserver;
private BroadcastReceiver mPackageChangeReceiver;
private BroadcastReceiver mUserRemovedReceiver;
- @Mock private DefaultDialerCache.DefaultDialerManagerAdapter mMockDefaultDialerManager;
- @Mock private RoleManagerAdapter mRoleManagerAdapter;
+ @Mock
+ private DefaultDialerCache.DefaultDialerManagerAdapter mMockDefaultDialerManager;
+ @Mock
+ private RoleManagerAdapter mRoleManagerAdapter;
@Override
@Before
@@ -75,18 +79,19 @@
mDefaultDialerCache = new DefaultDialerCache(
mContext, mMockDefaultDialerManager, mRoleManagerAdapter,
- new TelecomSystem.SyncRoot() { });
+ new TelecomSystem.SyncRoot() {
+ });
verify(mContext, times(2)).registerReceiverAsUser(
- packageReceiverCaptor.capture(), eq(UserHandle.ALL), any(IntentFilter.class),
+ packageReceiverCaptor.capture(), eq(UserHandle.ALL), any(IntentFilter.class),
isNull(String.class), isNull(Handler.class));
// Receive the first receiver that was captured, the package change receiver.
mPackageChangeReceiver = packageReceiverCaptor.getAllValues().get(0);
ArgumentCaptor<BroadcastReceiver> userRemovedReceiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
verify(mContext).registerReceiver(
- userRemovedReceiverCaptor.capture(), any(IntentFilter.class));
+ userRemovedReceiverCaptor.capture(), any(IntentFilter.class));
mUserRemovedReceiver = userRemovedReceiverCaptor.getAllValues().get(0);
mDefaultDialerSettingObserver = mDefaultDialerCache.getContentObserver();
@@ -139,7 +144,10 @@
Intent packageChangeIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED,
Uri.fromParts("package", DIALER1, null));
when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER0))).thenReturn(DIALER2);
+
mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+ waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER1));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
@@ -157,6 +165,8 @@
Intent packageChangeIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED,
Uri.fromParts("package", "red.orange.blue", null));
mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+ waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER1));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
@@ -191,6 +201,8 @@
packageChangeIntent.putExtra(Intent.EXTRA_REPLACING, false);
mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+ waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER1));
verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER2));
@@ -207,6 +219,8 @@
Uri.fromParts("package", "ppp.qqq.zzz", null));
mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+ waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER1));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
@@ -224,6 +238,8 @@
packageChangeIntent.putExtra(Intent.EXTRA_REPLACING, true);
mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+ waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER0));
verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER1));
verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER2));
@@ -239,7 +255,9 @@
when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER0))).thenReturn(DIALER2);
when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER1))).thenReturn(DIALER2);
when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER2))).thenReturn(DIALER2);
+
mDefaultDialerSettingObserver.onChange(false);
+ waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
diff --git a/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java b/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
index 2ab4e78..097061b 100644
--- a/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
@@ -26,11 +26,11 @@
import android.net.Uri;
import android.provider.CallLog;
import android.telecom.CallerInfo;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallerInfoLookupHelper;
-import com.android.server.telecom.callfiltering.CallFilter;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
@@ -44,7 +44,6 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
-
@RunWith(JUnit4.class)
public class DirectToVoicemailFilterTest extends TelecomTestCase {
@Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
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/DtmfLocalTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
index 85a5278..5ccfc38 100644
--- a/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
@@ -15,8 +15,15 @@
package com.android.server.telecom.tests;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.media.ToneGenerator;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.DtmfLocalTonePlayer;
@@ -29,12 +36,6 @@
import org.junit.runners.JUnit4;
import org.mockito.Mock;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class DtmfLocalTonePlayerTest extends TelecomTestCase {
private static final int TIMEOUT = 2000;
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
index 3cb8196..41426c0 100644
--- a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
@@ -17,7 +17,7 @@
package com.android.server.telecom.tests;
-import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+import static android.telephony.TelephonyManager.EmergencyCallDiagnosticData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@@ -35,6 +35,7 @@
import android.net.Uri;
import android.os.BugreportManager;
import android.os.DropBoxManager;
+import android.os.UserHandle;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -128,6 +129,7 @@
when(mTimeouts.getDaysBackToSearchEmergencyDiagnosticEntries()).
thenReturn(DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES);
when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
mEmergencyCallDiagnosticLogger = new EmergencyCallDiagnosticLogger(mTm, mBrm,
mTimeouts, mDbm, Runnable::run, mClockProxy);
@@ -171,7 +173,8 @@
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mMockClockProxy,
- mMockToastProxy);
+ mMockToastProxy,
+ mFeatureFlags);
}
/**
@@ -235,16 +238,16 @@
mEmergencyCallDiagnosticLogger.reportStuckCall(call);
//for stuck calls, we should always be persisting some data
- ArgumentCaptor<EmergencyCallDiagnosticParams> captor =
- ArgumentCaptor.forClass(EmergencyCallDiagnosticParams.class);
+ ArgumentCaptor<EmergencyCallDiagnosticData> captor =
+ ArgumentCaptor.forClass(EmergencyCallDiagnosticData.class);
verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
captor.capture());
- EmergencyCallDiagnosticParams dp = captor.getValue();
+ EmergencyCallDiagnosticData ecdData = captor.getValue();
- assertNotNull(dp);
+ assertNotNull(ecdData);
assertTrue(
- dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
- || dp.isTelephonyDumpSysCollectionEnabled());
+ ecdData.isLogcatCollectionEnabled() || ecdData.isTelecomDumpsysCollectionEnabled()
+ || ecdData.isTelephonyDumpsysCollectionEnabled());
//tracking should end
assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
@@ -262,17 +265,16 @@
mEmergencyCallDiagnosticLogger.onCallRemoved(call);
//for non-local disconnect of non-active call, we should always be persisting some data
- ArgumentCaptor<TelephonyManager.EmergencyCallDiagnosticParams> captor =
- ArgumentCaptor.forClass(
- TelephonyManager.EmergencyCallDiagnosticParams.class);
+ ArgumentCaptor<EmergencyCallDiagnosticData> captor =
+ ArgumentCaptor.forClass(EmergencyCallDiagnosticData.class);
verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
captor.capture());
- TelephonyManager.EmergencyCallDiagnosticParams dp = captor.getValue();
+ EmergencyCallDiagnosticData ecdData = captor.getValue();
- assertNotNull(dp);
+ assertNotNull(ecdData);
assertTrue(
- dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
- || dp.isTelephonyDumpSysCollectionEnabled());
+ ecdData.isLogcatCollectionEnabled() || ecdData.isTelecomDumpsysCollectionEnabled()
+ || ecdData.isTelephonyDumpsysCollectionEnabled());
//tracking should end
assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
index 692d720..cc1c38a 100644
--- a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
@@ -19,12 +19,24 @@
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+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.ContentResolver;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.DefaultDialerCache;
@@ -39,18 +51,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertFalse;
-
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doThrow;
-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 static org.mockito.Mockito.any;
-
@RunWith(JUnit4.class)
public class EmergencyCallHelperTest extends TelecomTestCase {
private static final String SYSTEM_DIALER_PACKAGE = "abc.xyz";
@@ -75,7 +75,7 @@
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
when(mContext.getPackageManager()).thenReturn(mPackageManager);
mEmergencyCallHelper = new EmergencyCallHelper(mContext, mDefaultDialerCache,
- mTimeoutsAdapter);
+ mTimeoutsAdapter, mFeatureFlags);
when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYSTEM_DIALER_PACKAGE);
//start with no perms
@@ -185,6 +185,61 @@
@SmallTest
@Test
+ public void testPermGrantAndRevokeForEmergencyCall() {
+
+ when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should be granted then revoked
+ verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testPermGrantAndRevokeForMultiEmergencyCall() {
+
+ when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+ //first call is emergency call
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ //second call is emergency call
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should be granted then revoked
+ verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testPermGrantAndRevokeForEmergencyCallAndNormalCall() {
+
+ when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+ //first call is emergency call
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ //second call is normal call
+ when(mCall.isEmergencyCall()).thenReturn(false);
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should be granted then revoked
+ verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
public void testNoPermGrantForNonEmergencyCall() {
when(mCall.isEmergencyCall()).thenReturn(false);
diff --git a/tests/src/com/android/server/telecom/tests/EventManagerTest.java b/tests/src/com/android/server/telecom/tests/EventManagerTest.java
index c7d3541..cee0f39 100644
--- a/tests/src/com/android/server/telecom/tests/EventManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/EventManagerTest.java
@@ -16,11 +16,17 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
import android.net.Uri;
import android.os.Build;
import android.telecom.Log;
import android.telecom.Logging.EventManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
@@ -32,11 +38,6 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
/**
* Unit tests for android.telecom.Logging.EventManager.
*/
diff --git a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
index 0bfa987..b7e5921 100644
--- a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
+++ b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
@@ -16,17 +16,26 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.eq;
+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.Intent;
import android.media.session.MediaSession;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
import android.view.KeyEvent;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.HeadsetMediaButton;
-import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.HeadsetMediaButton.MediaSessionWrapper;
+import com.android.server.telecom.TelecomSystem;
import org.junit.After;
import org.junit.Before;
@@ -37,15 +46,6 @@
import org.mockito.Mock;
import org.mockito.Mockito;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class HeadsetMediaButtonTest extends TelecomTestCase {
private static final int TEST_TIMEOUT_MILLIS = 1000;
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 683a5e2..bea3fe3 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -19,7 +19,6 @@
import static com.android.server.telecom.InCallController.IN_CALL_SERVICE_NOTIFICATION_ID;
import static com.android.server.telecom.InCallController.NOTIFICATION_TAG;
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.assertNotNull;
@@ -28,15 +27,13 @@
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyObject;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.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.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -53,6 +50,7 @@
import android.app.Notification;
import android.app.NotificationManager;
import android.app.UiModeManager;
+import android.compat.testing.PlatformCompatChangeRule;
import android.content.AttributionSource;
import android.content.AttributionSourceState;
import android.content.BroadcastReceiver;
@@ -70,7 +68,6 @@
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
-import android.compat.testing.PlatformCompatChangeRule;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -82,21 +79,24 @@
import android.os.UserManager;
import android.permission.PermissionCheckerManager;
import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
import android.telecom.InCallService;
import android.telecom.ParcelableCall;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.test.mock.MockContext;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
import android.text.TextUtils;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.telecom.IInCallAdapter;
import com.android.internal.telecom.IInCallService;
import com.android.server.telecom.Analytics;
import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallEndpointController;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.CarModeTracker;
import com.android.server.telecom.ClockProxy;
@@ -143,6 +143,8 @@
@Mock PackageManager mMockPackageManager;
@Mock PermissionCheckerManager mMockPermissionCheckerManager;
@Mock Call mMockCall;
+ @Mock Call mMockSystemCall1;
+ @Mock Call mMockSystemCall2;
@Mock Resources mMockResources;
@Mock AppOpsManager mMockAppOpsManager;
@Mock MockContext mMockContext;
@@ -154,10 +156,10 @@
@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;
+ @Mock CallEndpointController mMockCallEndpointController;
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -184,6 +186,9 @@
private static final String APPOP_NONUI_PKG = "appop_nonui_pkg";
private static final String APPOP_NONUI_CLASS = "appop_nonui_cls";
private static final int APPOP_NONUI_UID = 7;
+ private static final String BT_PKG = "btpkg";
+ private static final String BT_CLS = "btcls";
+ private static final int BT_UID = 900974;
private static final PhoneAccountHandle PA_HANDLE =
new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"),
@@ -223,8 +228,9 @@
when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYS_PKG);
when(mDefaultDialerCache.getSystemDialerComponent()).thenReturn(
new ComponentName(SYS_PKG, SYS_CLASS));
+ when(mDefaultDialerCache.getBTInCallServicePackages()).thenReturn(new String[] {BT_PKG});
mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
- mTimeoutsAdapter);
+ mTimeoutsAdapter, mFeatureFlags);
when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
.thenReturn(mNotificationManager);
@@ -236,7 +242,7 @@
"com.android.server.telecom.tests", null));
mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
- mEmergencyCallHelper, mCarModeTracker, mClockProxy);
+ mEmergencyCallHelper, mCarModeTracker, mClockProxy, mFeatureFlags);
// Capture the broadcast receiver registered.
doAnswer(invocation -> {
mRegisteredReceiver = invocation.getArgument(0);
@@ -267,6 +273,8 @@
return new String[] { NONUI_PKG };
case APPOP_NONUI_UID:
return new String[] { APPOP_NONUI_PKG };
+ case BT_UID:
+ return new String[] { BT_PKG };
}
return null;
}).when(mMockPackageManager).getPackagesForUid(anyInt());
@@ -302,13 +310,23 @@
.thenReturn(PackageManager.PERMISSION_DENIED);
when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0));
+ when(mFeatureFlags.onCallEndpointChangedIcsOnConnected()).thenReturn(true);
+ when(mMockCallsManager.getCallEndpointController()).thenReturn(mMockCallEndpointController);
+ when(mMockCallEndpointController.getCurrentCallEndpoint())
+ .thenReturn(new CallEndpoint("Earpiece", 1));
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);
}
@Override
@@ -517,7 +535,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);
@@ -588,9 +607,11 @@
when(mMockCall.isEmergencyCall()).thenReturn(true);
when(mMockCall.isIncoming()).thenReturn(true);
when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
@@ -617,9 +638,11 @@
when(mMockCall.isInECBM()).thenReturn(true);
when(mMockCall.isIncoming()).thenReturn(true);
when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+ .thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
@@ -647,10 +670,12 @@
when(mMockCall.isInECBM()).thenReturn(true);
when(mMockCall.isIncoming()).thenReturn(true);
when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockContext.getSystemService(eq(UserManager.class)))
.thenReturn(mMockUserManager);
- when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
- 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 */);
@@ -677,8 +702,9 @@
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 */);
@@ -712,7 +738,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);
@@ -990,7 +1017,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);
@@ -1100,7 +1128,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);
@@ -1776,7 +1805,7 @@
// 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);
@@ -1839,7 +1868,66 @@
assertNull(mInCallController.getInCallServiceConnections().get(testUser));
}
- private void setupMocksForWorkProfileTest() {
+ @Test
+ public void testRemoveAllServiceConnections_MultiUser() throws Exception {
+ when(mFeatureFlags.associatedUserRefactorForWorkProfile()).thenReturn(true);
+ setupMocks(false /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ UserHandle workUser = new UserHandle(12);
+ when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(workUser);
+ setupFakeSystemCall(mMockSystemCall1, 1);
+ setupFakeSystemCall(mMockSystemCall2, 2);
+
+ // Add "work" call to service. The mapping should've been inserted
+ // with the workUser as the key.
+ mInCallController.onCallAdded(mMockCall);
+ // Add system call to service. The mapping should've been
+ // inserted with the system user as the key.
+ mInCallController.onCallAdded(mMockSystemCall1);
+
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ // Make sure we bound to the system call as well as the work call.
+ verify(mMockContext, times(2)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(UserHandle.CURRENT));
+ assertTrue(mInCallController.getInCallServiceConnections().containsKey(workUser));
+ assertTrue(mInCallController.getInCallServiceConnections().containsKey(UserHandle.SYSTEM));
+
+ // Remove the work call. This leverages getUserFromCall to remove the ICS mapping.
+ when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockSystemCall1));
+ mInCallController.onCallRemoved(mMockCall);
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+ // Verify that the mapping was properly removed.
+ assertNull(mInCallController.getInCallServiceConnections().get(workUser));
+ // Verify mapping for system user is still present.
+ assertNotNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM));
+
+ // Add another system call
+ mInCallController.onCallAdded(mMockSystemCall2);
+ when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockSystemCall2));
+ // Remove first system call and verify that mapping is present
+ mInCallController.onCallRemoved(mMockSystemCall1);
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+ // Verify mapping for system user is still present.
+ assertNotNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM));
+ // Remove last system call and verify that connection isn't present in ICS mapping.
+ when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList());
+ mInCallController.onCallRemoved(mMockSystemCall2);
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+ assertNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM));
+ }
+
+ private void setupFakeSystemCall(@Mock Call call, int id) {
+ when(call.getAssociatedUser()).thenReturn(UserHandle.SYSTEM);
+ when(call.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+ when(call.getAnalytics()).thenReturn(new Analytics.CallInfo());
+ when(call.getId()).thenReturn("TC@" + id);
+ }
+
+ private void setupMocksForProfileTest() {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockChildUserCall.isIncoming()).thenReturn(false);
@@ -1852,27 +1940,34 @@
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(true);
when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
- when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mChildUserHandle);
- when(mMockUserManager.getProfileParent(mChildUserHandle.getIdentifier())).thenReturn(
- mMockUserInfo);
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
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(mChildUserHandle.getIdentifier())).thenReturn(true);
- when(mMockUserManager.isManagedProfile(mParentUserHandle.getIdentifier())).thenReturn(
- false);
+ when(mFeatureFlags.profileUserSupport()).thenReturn(true);
+ }
+
+ /**
+ * Verify that if a null inCallService object is passed to sendCallToInCallService, a
+ * NullPointerException is not thrown.
+ */
+ @Test
+ public void testSendCallToInCallServiceWithNullService() {
+ when(mFeatureFlags.doNotSendCallToNullIcs()).thenReturn(true);
+ //Setup up parent and child/work profile relation
+ when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
+ when(mMockUserManager.getProfileParent(mChildUserHandle)).thenReturn(mParentUserHandle);
+ when(mFeatureFlags.profileUserSupport()).thenReturn(true);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ // verify a NullPointerException is not thrown
+ int res = mInCallController.sendCallToService(mMockCall, mInCallServiceInfo, null);
+ assertEquals(0, res);
}
@Test
- public void testManagedProfileCallQueriesIcsUsingParentUserToo() throws Exception {
- setupMocksForWorkProfileTest();
+ public void testProfileCallQueriesIcsUsingParentUserToo() throws Exception {
+ setupMocksForProfileTest();
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
setupMockPackageManager(true /* default */,
true /*useNonUiInCalls*/, true /*useAppOpNonUiInCalls*/,
@@ -1881,9 +1976,8 @@
true /*includeSelfManagedCallsInCarModeDialer*/,
true /*includeSelfManagedCallsInNonUi*/);
- //pass in call by child/work-profileuser
+ //pass in call by child/profile user
mInCallController.bindToServices(mMockChildUserCall);
-
// Verify that queryIntentServicesAsUser is also called with parent handle
// Query for the different InCallServices
ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1901,6 +1995,34 @@
userIds.contains(mParentUserHandle.getIdentifier()));
}
+ @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.getBTInCallServicePackages()[0]);
+ LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
+ resolveInfo.add(getBluetoothResolveinfo());
+ when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ LinkedList<ResolveInfo> resolveInfo1 = new LinkedList<ResolveInfo>();
+ Intent intent = (Intent) args[0];
+ if (intent.getAction().equals(InCallService.SERVICE_INTERFACE)) {
+ resolveInfo1.add(getBluetoothResolveinfo());
+ }
+ return resolveInfo1;
+ }).when(mMockPackageManager).queryIntentServicesAsUser(any(Intent.class), anyInt(),
+ anyInt());
+
+ mInCallController.bindToBTService(mMockCall, null);
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext).bindServiceAsUser(captor.capture(), any(ServiceConnection.class),
+ anyInt(), any(UserHandle.class));
+ }
+
private void setupMocks(boolean isExternalCall) {
setupMocks(isExternalCall, false /* isSelfManagedCall */);
}
@@ -2025,6 +2147,18 @@
}};
}
+ private ResolveInfo getBluetoothResolveinfo() {
+ return new ResolveInfo() {{
+ serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = BT_PKG;
+ serviceInfo.name = BT_CLS;
+ serviceInfo.applicationInfo = new ApplicationInfo();
+ serviceInfo.applicationInfo.uid = BT_UID;
+ serviceInfo.enabled = true;
+ serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+ }};
+ }
+
private void setupMockPackageManager(final boolean useDefaultDialer,
final boolean useSystemDialer, final boolean includeExternalCalls) {
setupMockPackageManager(useDefaultDialer, false, false, useSystemDialer, includeExternalCalls,
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index 88b5bb5..39381e6 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -16,6 +16,7 @@
package com.android.server.telecom.tests;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IInCallAdapter;
import com.android.internal.telecom.IInCallService;
@@ -39,7 +40,7 @@
* Controls a test {@link IInCallService} as would be provided by an InCall UI on a system.
*/
public class InCallServiceFixture implements TestFixture<IInCallService> {
-
+ public static boolean sIgnoreOverrideAdapterFlag = false;
public String mLatestCallId;
public IInCallAdapter mInCallAdapter;
public CallAudioState mCallAudioState;
@@ -53,10 +54,17 @@
public CountDownLatch mUpdateCallLock = new CountDownLatch(1);
public CountDownLatch mAddCallLock = new CountDownLatch(1);
+ @VisibleForTesting
+ public static void setIgnoreOverrideAdapterFlag(boolean flag) {
+ sIgnoreOverrideAdapterFlag = flag;
+ }
+
public class FakeInCallService extends IInCallService.Stub {
@Override
public void setInCallAdapter(IInCallAdapter inCallAdapter) throws RemoteException {
- if (mInCallAdapter != null && inCallAdapter != null) {
+ // sIgnoreOverrideAdapterFlag is being used to verify a scenario where the InCallAdapter
+ // gets set twice (secondary user places MO/MT call).
+ if (mInCallAdapter != null && inCallAdapter != null && !sIgnoreOverrideAdapterFlag) {
throw new RuntimeException("Adapter is already set");
}
if (mInCallAdapter == null && inCallAdapter == null) {
diff --git a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
index 1f1b939..df26684 100644
--- a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
@@ -23,29 +23,29 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.never;
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.media.AudioManager;
import android.media.MediaPlayer;
import android.media.ToneGenerator;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.Call;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.DockManager;
import com.android.server.telecom.InCallTonePlayer;
import com.android.server.telecom.TelecomSystem;
-import com.android.server.telecom.Timeouts;
import com.android.server.telecom.WiredHeadsetManager;
import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
import org.junit.After;
import org.junit.Before;
@@ -63,7 +63,6 @@
@Mock private BluetoothRouteManager mBluetoothRouteManager;
@Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
- @Mock private Timeouts.Adapter mTimeoutsAdapter;
@Mock private BluetoothDeviceManager mBluetoothDeviceManager;
@Mock private TelecomSystem.SyncRoot mLock;
@Mock private ToneGenerator mToneGenerator;
@@ -72,7 +71,6 @@
@Mock private DockManager mDockManager;
@Mock private AsyncRingtonePlayer mRingtonePlayer;
@Mock private BluetoothDevice mDevice;
- @Mock private BluetoothAdapter mBluetoothAdapter;
private InCallTonePlayer.MediaPlayerAdapter mMediaPlayerAdapter =
new InCallTonePlayer.MediaPlayerAdapter() {
@@ -112,7 +110,8 @@
@Mock
private CallAudioManager mCallAudioManager;
-
+ @Mock
+ private Call mCall;
private InCallTonePlayer mInCallTonePlayer;
@Override
@@ -122,15 +121,15 @@
when(mToneGeneratorFactory.get(anyInt(), anyInt())).thenReturn(mToneGenerator);
when(mMediaPlayerFactory.get(anyInt(), any())).thenReturn(mMediaPlayerAdapter);
- doNothing().when(mCallAudioManager).setIsTonePlaying(anyBoolean());
+ doNothing().when(mCallAudioManager).setIsTonePlaying(any(Call.class), anyBoolean());
mCallAudioRoutePeripheralAdapter = new CallAudioRoutePeripheralAdapter(
mCallAudioRouteStateMachine, mBluetoothRouteManager, mWiredHeadsetManager,
mDockManager, mRingtonePlayer);
mFactory = new InCallTonePlayer.Factory(mCallAudioRoutePeripheralAdapter, mLock,
- mToneGeneratorFactory, mMediaPlayerFactory, mAudioManagerAdapter);
+ mToneGeneratorFactory, mMediaPlayerFactory, mAudioManagerAdapter, mFeatureFlags);
mFactory.setCallAudioManager(mCallAudioManager);
- mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_CALL_ENDED);
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_CALL_ENDED);
}
@Override
@@ -147,11 +146,12 @@
assertTrue(mInCallTonePlayer.startTone());
// Verify we did play a tone.
verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
mInCallTonePlayer.stopTone();
// Timeouts due to threads!
- verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+ eq(false));
}
@SmallTest
@@ -161,11 +161,12 @@
assertTrue(mInCallTonePlayer.startTone());
// Verify we did play a tone.
verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
mInCallTonePlayer.stopTone();
// Timeouts due to threads!
- verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+ eq(false));
// Correctness check: ensure we can't start the tone again.
assertFalse(mInCallTonePlayer.startTone());
@@ -174,15 +175,16 @@
@SmallTest
@Test
public void testInterruptToneGenerator() {
- mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
assertTrue(mInCallTonePlayer.startTone());
verify(mToneGenerator, timeout(TEST_TIMEOUT)).startTone(anyInt());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
mInCallTonePlayer.stopTone();
// Timeouts due to threads!
- verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+ eq(false));
// Ideally it would be nice to verify this, however release is a native method so appears to
// cause flakiness when testing on Cuttlefish.
// verify(mToneGenerator, timeout(TEST_TIMEOUT)).release();
@@ -199,78 +201,126 @@
// Verify we did play a tone.
verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
- verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+ eq(true));
}
+ /**
+ * Only applicable when {@link FeatureFlags#useStreamVoiceCallTones()} is false and we use
+ * STREAM_BLUETOOTH_SCO for tones.
+ */
@SmallTest
@Test
public void testRingbackToneAudioStreamHeadset() {
+ when(mFeatureFlags.useStreamVoiceCallTones()).thenReturn(false);
when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
- mBluetoothDeviceManager.setBluetoothRouteManager(mBluetoothRouteManager);
- when(mBluetoothRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(mDevice);
- when(mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+ setConnectedBluetoothDevice(false /*isLe*/, false /*isHearingAid*/);
- when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
- when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(false);
-
- mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
assertTrue(mInCallTonePlayer.startTone());
+
verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
.get(eq(AudioManager.STREAM_BLUETOOTH_SCO), anyInt());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
}
+ /**
+ * Only applicable when {@link FeatureFlags#useStreamVoiceCallTones()} is false and we use
+ * STREAM_BLUETOOTH_SCO for tones.
+ */
@SmallTest
@Test
public void testCallWaitingToneAudioStreamHeadset() {
+ when(mFeatureFlags.useStreamVoiceCallTones()).thenReturn(false);
when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
- mBluetoothDeviceManager.setBluetoothRouteManager(mBluetoothRouteManager);
- when(mBluetoothRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(mDevice);
- when(mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+ setConnectedBluetoothDevice(false /*isLe*/, false /*isHearingAid*/);
- when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
- when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(false);
-
- mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_CALL_WAITING);
assertTrue(mInCallTonePlayer.startTone());
+
verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
.get(eq(AudioManager.STREAM_BLUETOOTH_SCO), anyInt());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
+ }
+
+
+ /**
+ * Only applicable when {@link FeatureFlags#useStreamVoiceCallTones()} is true and we use
+ * STREAM_VOICE_CALL for ALL tones.
+ */
+ @SmallTest
+ @Test
+ public void testRingbackToneAudioStreamSco() {
+ when(mFeatureFlags.useStreamVoiceCallTones()).thenReturn(true);
+ when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
+ setConnectedBluetoothDevice(false /*isLe*/, false /*isHearingAid*/);
+
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
+ assertTrue(mInCallTonePlayer.startTone());
+
+ verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
+ .get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
+ }
+
+ /**
+ * Only applicable when {@link FeatureFlags#useStreamVoiceCallTones()} is true and we use
+ * STREAM_VOICE_CALL for ALL tones.
+ */
+ @SmallTest
+ @Test
+ public void testRingbackToneAudioStreamLe() {
+ when(mFeatureFlags.useStreamVoiceCallTones()).thenReturn(true);
+ when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
+ setConnectedBluetoothDevice(true /*isLe*/, false /*isHearingAid*/);
+
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
+ assertTrue(mInCallTonePlayer.startTone());
+
+ verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
+ .get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
}
@SmallTest
@Test
public void testRingbackToneAudioStreamHearingAid() {
when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
- mBluetoothDeviceManager.setBluetoothRouteManager(mBluetoothRouteManager);
- when(mBluetoothRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(mDevice);
- when(mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+ setConnectedBluetoothDevice(false /*isLe*/, true /*isHearingAid*/);
- when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
- when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(true);
-
- mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
assertTrue(mInCallTonePlayer.startTone());
+
verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
.get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
}
@SmallTest
@Test
public void testCallWaitingToneAudioStreamHearingAid() {
when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
+ setConnectedBluetoothDevice(false /*isLe*/, true /*isHearingAid*/);
+
+ mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_CALL_WAITING);
+ assertTrue(mInCallTonePlayer.startTone());
+
+ verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
+ .get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
+ verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
+ }
+
+ /**
+ * Set a connected BT device. If not LE or Hearing Aid, it will be configured as SCO
+ * @param isLe true if LE
+ * @param isHearingAid true if hearing aid
+ */
+ private void setConnectedBluetoothDevice(boolean isLe, boolean isHearingAid) {
mBluetoothDeviceManager.setBluetoothRouteManager(mBluetoothRouteManager);
when(mBluetoothRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(mDevice);
when(mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
- when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
- when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(true);
-
- mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
- assertTrue(mInCallTonePlayer.startTone());
- verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
- .get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
- verify(mCallAudioManager).setIsTonePlaying(eq(true));
+ when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(isLe);
+ when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(isHearingAid);
}
}
diff --git a/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java b/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
index f935908..cdf2542 100644
--- a/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
@@ -17,13 +17,14 @@
package com.android.server.telecom.tests;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.never;
import android.os.PowerManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
index 9269836..d7905b2 100644
--- a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
@@ -16,29 +16,36 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
+import com.android.server.telecom.Ringer;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.callfiltering.CallFilter;
import com.android.server.telecom.callfiltering.CallFilterResultCallback;
import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.callfiltering.DndCallFilter;
import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.when;
-
import org.junit.Before;
+import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
-import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@@ -46,6 +53,7 @@
@RunWith(JUnit4.class)
public class IncomingCallFilterGraphTest extends TelecomTestCase {
+ private final String TAG = IncomingCallFilterGraphTest.class.getSimpleName();
@Mock private Call mCall;
@Mock private Context mContext;
@Mock private Timeouts.Adapter mTimeoutsAdapter;
@@ -87,13 +95,15 @@
@Override
public CompletionStage<CallFilteringResult> startFilterLookup(
CallFilteringResult priorStageResult) {
- HandlerThread handlerThread = new HandlerThread("TimeoutFilter");
- handlerThread.start();
- Handler handler = new Handler(handlerThread.getLooper());
-
- CompletableFuture<CallFilteringResult> resultFuture = new CompletableFuture<>();
- handler.postDelayed(() -> resultFuture.complete(PASS_CALL_RESULT),
- TIMEOUT_FILTER_SLEEP_TIME);
+ Log.i(TAG, "TimeoutFilter: startFilterLookup: about to sleep");
+ try {
+ // Currently, there are no tools to fake a timeout with [CompletableFuture]s
+ // in the Android Platform. Thread sleep is the best option for an end-to-end test.
+ Thread.sleep(FILTER_TIMEOUT); // Simulate a filter timeout
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ Log.i(TAG, "TimeoutFilter: startFilterLookup: continuing test");
return CompletableFuture.completedFuture(PASS_CALL_RESULT);
}
}
@@ -115,7 +125,7 @@
CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
- mTimeoutsAdapter, mLock);
+ mTimeoutsAdapter, mFeatureFlags, mLock);
graph.performFiltering();
assertEquals(PASS_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
@@ -128,7 +138,7 @@
CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
- mTimeoutsAdapter, mLock);
+ mTimeoutsAdapter, mFeatureFlags, mLock);
AllowFilter allowFilter = new AllowFilter();
DisallowFilter disallowFilter = new DisallowFilter();
graph.addFilter(allowFilter);
@@ -146,7 +156,7 @@
CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
- mTimeoutsAdapter, mLock);
+ mTimeoutsAdapter, mFeatureFlags, mLock);
AllowFilter allowFilter1 = new AllowFilter();
AllowFilter allowFilter2 = new AllowFilter();
DisallowFilter disallowFilter = new DisallowFilter();
@@ -165,7 +175,7 @@
CallFilterResultCallback listener = (call, result, timeout) -> testResult.complete(result);
IncomingCallFilterGraph graph = new IncomingCallFilterGraph(mCall, listener, mContext,
- mTimeoutsAdapter, mLock);
+ mTimeoutsAdapter, mFeatureFlags, mLock);
DisallowFilter disallowFilter = new DisallowFilter();
TimeoutFilter timeoutFilter = new TimeoutFilter();
graph.addFilter(disallowFilter);
@@ -175,4 +185,57 @@
assertEquals(REJECT_CALL_RESULT, testResult.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
}
+
+ /**
+ * Verify that when the Call Filtering Graph times out, already completed filters are combined.
+ * Graph being tested:
+ *
+ * startFilterLookup --> [ ALLOW_FILTER ]
+ * |
+ * ---------------------------------
+ * | |
+ * | |
+ * [DND_FILTER] [TIMEOUT_FILTER]
+ * | |
+ * | * timeout at 5 seconds *
+ * |
+ * |
+ * --------[ CallFilteringResult ]
+ */
+ @SmallTest
+ @Test
+ public void testFilterTimesOutWithDndFilterComputedAlready() throws Exception {
+ // GIVEN: a graph that is set up like the above diagram in the test comment
+ Ringer mockRinger = mock(Ringer.class);
+ CompletableFuture<CallFilteringResult> testResult = new CompletableFuture<>();
+ IncomingCallFilterGraph graph = new IncomingCallFilterGraph(
+ mCall,
+ (call, result, timeout) -> testResult.complete(result),
+ mContext,
+ mTimeoutsAdapter,
+ mFeatureFlags,
+ mLock);
+ // create the filters / nodes for the graph
+ TimeoutFilter timeoutFilter = new TimeoutFilter();
+ DndCallFilter dndCallFilter = new DndCallFilter(mCall, mockRinger);
+ AllowFilter allowFilter1 = new AllowFilter();
+ // adding them to the graph does not create the edges
+ graph.addFilter(allowFilter1);
+ graph.addFilter(timeoutFilter);
+ graph.addFilter(dndCallFilter);
+ // set up the graph so that the DND filter can process in parallel to the timeout
+ IncomingCallFilterGraph.addEdge(allowFilter1, dndCallFilter);
+ IncomingCallFilterGraph.addEdge(allowFilter1, timeoutFilter);
+
+ // WHEN: DND is on and the caller cannot interrupt and the graph is processed
+ when(mockRinger.shouldRingForContact(mCall)).thenReturn(false);
+ when(mFeatureFlags.checkCompletedFiltersOnTimeout()).thenReturn(true);
+ dndCallFilter.startFilterLookup(IncomingCallFilterGraph.DEFAULT_RESULT);
+ graph.performFiltering();
+
+ // THEN: assert shouldSuppressCallDueToDndStatus is true!
+ assertFalse(IncomingCallFilterGraph.DEFAULT_RESULT.shouldSuppressCallDueToDndStatus);
+ assertTrue(testResult.get(TIMEOUT_FILTER_SLEEP_TIME,
+ TimeUnit.MILLISECONDS).shouldSuppressCallDueToDndStatus);
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
index 914fdc5..2d81bb3 100644
--- a/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
@@ -16,15 +16,21 @@
package com.android.server.telecom.tests;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.app.NotificationManager;
-import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.UserHandle;
-import android.telecom.PhoneAccountHandle;
import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
@@ -38,14 +44,6 @@
import org.junit.runners.JUnit4;
import org.mockito.Mock;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
/**
* Tests for the {@link com.android.server.telecom.ui.IncomingCallNotifier} class.
*/
diff --git a/tests/src/com/android/server/telecom/tests/LogUtilsTest.java b/tests/src/com/android/server/telecom/tests/LogUtilsTest.java
index 637dfbc..4393d90 100644
--- a/tests/src/com/android/server/telecom/tests/LogUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/LogUtilsTest.java
@@ -18,7 +18,7 @@
import static org.junit.Assert.assertTrue;
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.LogUtils;
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 2b05430..1776411 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -16,9 +16,30 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.ui.MissedCallNotifierImpl.CALL_LOG_COLUMN_ID;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
@@ -29,8 +50,6 @@
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@@ -39,13 +58,14 @@
import android.os.Looper;
import android.os.UserHandle;
import android.provider.CallLog;
+import android.telecom.CallerInfo;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.telecom.CallerInfo;
+import androidx.test.filters.SmallTest;
+
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.Constants;
import com.android.server.telecom.DefaultDialerCache;
@@ -67,32 +87,11 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-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.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class MissedCallNotifierImplTest extends TelecomTestCase {
@@ -241,7 +240,7 @@
MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
CALL_TIMESTAMP, phoneAccount.getAccountHandle());
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(),
anyString(), any());
@@ -250,6 +249,31 @@
assertEquals(1, sentIntent.getIntExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, -1));
}
+ @SmallTest
+ @Test
+ public void testCallLogUriSentToNotifier(){
+ MissedCallNotifier missedCallNotifier = setupMissedCallNotificationThroughDefaultDialer();
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+ Cursor mockMissedCallsCursor = new MockMissedCallCursorBuilder()
+ .addEntry(TEL_CALL_HANDLE.getSchemeSpecificPart(),
+ CallLog.Calls.PRESENTATION_ALLOWED, CALL_TIMESTAMP)
+ .build();
+ MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
+ CALL_TIMESTAMP, phoneAccount.getAccountHandle());
+ when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true);
+
+ missedCallNotifier.showMissedCallNotification(fakeCall,
+ CallLog.Calls.CONTENT_URI.buildUpon().appendPath(Long.toString(
+ mockMissedCallsCursor.getInt(CALL_LOG_COLUMN_ID))).build());
+ ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(),
+ anyString(), any());
+
+ Intent sentIntent = intentArgumentCaptor.getValue();
+ Uri actualCallUri = sentIntent.getParcelableExtra(TelecomManager.EXTRA_CALL_LOG_URI);
+ assertTrue(actualCallUri.isPathPrefixMatch(CallLog.Calls.CONTENT_URI));
+ }
+
private MissedCallNotifier setupMissedCallNotificationThroughDefaultDialer() {
mComponentContextFixture.addIntentReceiver(
TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION, COMPONENT_NAME);
@@ -275,9 +299,9 @@
MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
CALL_TIMESTAMP, phoneAccount.getAccountHandle());
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */null);
missedCallNotifier.clearMissedCalls(userHandle);
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */null);
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(
Integer.class);
@@ -308,10 +332,10 @@
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
- mDeviceIdleControllerAdapter);
+ mDeviceIdleControllerAdapter, mFeatureFlags);
- missedCallNotifier.showMissedCallNotification(fakeCall);
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
// The following captor is to capture the two notifications that got passed into
// notifyAsUser. This distinguishes between the builders used for the full notification
@@ -402,7 +426,7 @@
MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
CALL_TIMESTAMP, phoneAccount.getAccountHandle());
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(
Notification.class);
@@ -464,13 +488,13 @@
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
- mDeviceIdleControllerAdapter);
+ mDeviceIdleControllerAdapter, mFeatureFlags);
PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
MissedCallNotifier.CallInfo fakeCall =
makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
phoneAccount.getAccountHandle());
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
// Create two intents that correspond to call-back and respond back with SMS, and assert
// that in the case of a SIP call, no SMS intent is generated.
@@ -525,7 +549,7 @@
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
- mDeviceIdleControllerAdapter);
+ mDeviceIdleControllerAdapter, mFeatureFlags);
// AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
// timeout-verify, so run this in a new handler to mitigate that.
@@ -595,7 +619,7 @@
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
- mDeviceIdleControllerAdapter);
+ mDeviceIdleControllerAdapter, mFeatureFlags);
// AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
// timeout-verify, so run this in a new handler to mitigate that.
@@ -637,13 +661,13 @@
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
- mDeviceIdleControllerAdapter);
+ mDeviceIdleControllerAdapter, mFeatureFlags);
PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
MissedCallNotifier.CallInfo fakeCall =
makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
phoneAccount.getAccountHandle());
- missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<Bundle> bundleCaptor =
@@ -668,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);
@@ -701,7 +744,7 @@
NotificationBuilderFactory fakeBuilderFactory, UserHandle currentUser) {
MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
- mDeviceIdleControllerAdapter);
+ mDeviceIdleControllerAdapter, mFeatureFlags);
missedCallNotifier.setCurrentUserHandle(currentUser);
return missedCallNotifier;
}
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
index e441835..c0e3435 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
@@ -16,14 +16,17 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
import android.content.ComponentName;
import android.net.Uri;
-import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.SmallTest;
import android.telecom.CallerInfo;
+import android.telecom.PhoneAccountHandle;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.MissedCallNotifier;
-import com.android.server.telecom.MissedCallNotifier.CallInfo;
import org.junit.After;
import org.junit.Before;
@@ -31,9 +34,6 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
@RunWith(JUnit4.class)
public class MissedCallNotifierTest extends TelecomTestCase {
private static final ComponentName COMPONENT_NAME =
diff --git a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
index 4af3de3..0c3588e 100644
--- a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
@@ -32,9 +32,9 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@@ -109,6 +109,7 @@
mAdapter = new CallIntentProcessor.AdapterImpl(mCallsManager.getDefaultDialerCache());
mNotificationManager = spy((NotificationManager) mContext.getSystemService(
Context.NOTIFICATION_SERVICE));
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
when(mContentResolver.getPackageName()).thenReturn(PACKAGE_NAME);
when(mContentResolver.acquireProvider(any(String.class))).thenReturn(mContentProvider);
when(mContentProvider.call(any(String.class), any(String.class),
@@ -152,6 +153,8 @@
setUpEmergencyCall();
when(mEmergencyCall.getAssociatedUser()).
thenReturn(mPhoneAccountA0.getAccountHandle().getUserHandle());
+ when(mEmergencyCall.getTargetPhoneAccount())
+ .thenReturn(mPhoneAccountA0.getAccountHandle());
mCallsManager.addCall(mEmergencyCall);
assertTrue(mCallsManager.isInEmergencyCall());
@@ -417,7 +420,7 @@
null, mCallsManager.getPhoneNumberUtilsAdapter(), null,
null, null, mPhoneAccountA0.getAccountHandle(),
Call.CALL_DIRECTION_INCOMING, false, false,
- mClockProxy, null));
+ mClockProxy, null, mFeatureFlags));
doReturn(1L).when(mIncomingCall).getStartRingTime();
doAnswer((x) -> {
mCountDownLatch.countDown();
diff --git a/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
index ed74637..3f4b5f6 100644
--- a/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
@@ -20,7 +20,8 @@
import static org.junit.Assert.assertTrue;
import android.net.Uri;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.MmiUtils;
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index 33acd98..e75ad97 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -20,17 +20,16 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNotNull;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -51,7 +50,8 @@
import android.telecom.VideoProfile;
import android.telephony.DisconnectCause;
import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
@@ -64,6 +64,7 @@
import com.android.server.telecom.RoleManagerAdapter;
import com.android.server.telecom.SystemStateHelper;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
import org.junit.After;
import org.junit.Before;
@@ -75,6 +76,8 @@
@RunWith(JUnit4.class)
public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
+ private static final Uri TEST_URI = Uri.parse("tel:16505551212");
+
private static class ReceiverIntentPair {
public BroadcastReceiver receiver;
public Intent intent;
@@ -93,6 +96,7 @@
@Mock private PhoneAccountRegistrar mPhoneAccountRegistrar;
@Mock private RoleManagerAdapter mRoleManagerAdapter;
@Mock private DefaultDialerCache mDefaultDialerCache;
+ @Mock private FeatureFlags mFeatureFlags;
@Mock private MmiUtils mMmiUtils;
private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl();
@@ -113,6 +117,7 @@
any(PhoneAccountHandle.class))).thenReturn(mPhoneAccount);
when(mPhoneAccount.isSelfManaged()).thenReturn(true);
when(mSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(false);
+ when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false);
}
@Override
@@ -510,6 +515,84 @@
testUnmodifiedRegularCall();
}
+ /**
+ * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered
+ * broadcast and did not try to start the call immediately (legacy behavior).
+ */
+ @SmallTest
+ @Test
+ public void testSendBroadcastBlocking() {
+ when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false);
+ Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI);
+ NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster(
+ mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
+ true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
+
+ NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall();
+ nocib.processCall(mCall, disposition);
+
+ // We should not have not short-circuited to place the outgoing call directly.
+ verify(mCall, never()).setNewOutgoingCallIntentBroadcastIsDone();
+ verify(mCallsManager, never()).placeOutgoingCall(any(Call.class), any(Uri.class),
+ any(GatewayInfo.class), anyBoolean(), anyInt());
+
+ // Ensure we did send the broadcast ordered
+ verifyBroadcastSent(TEST_URI.getSchemeSpecificPart(),
+ createNumberExtras(TEST_URI.getSchemeSpecificPart()));
+
+ // Ensure we did not try to directly send the broadcast unordered.
+ verify(mContext, never()).sendBroadcastAsUser(
+ any(Intent.class),
+ eq(UserHandle.CURRENT),
+ eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS));
+ }
+
+ /**
+ * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered
+ * broadcast and did not try to start the call immediately. Also ensure that the broadcast
+ * flags are correct.
+ */
+ @SmallTest
+ @Test
+ public void testSendBroadcastNonBlocking() {
+ when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(true);
+ Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI);
+ NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster(
+ mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
+ true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
+
+ NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall();
+ nocib.processCall(mCall, disposition);
+
+ // We should have started the outgoing call flow immediately.
+ verify(mCall).setNewOutgoingCallIntentBroadcastIsDone();
+ verify(mCallsManager).placeOutgoingCall(any(Call.class), any(Uri.class),
+ nullable(GatewayInfo.class), anyBoolean(), anyInt());
+
+ // Ensure we didn't send an ordered broadcast.
+ verify(mContext, never()).sendOrderedBroadcastAsUser(
+ any(Intent.class),
+ any(UserHandle.class),
+ anyString(),
+ anyInt(),
+ any(Bundle.class),
+ any(BroadcastReceiver.class),
+ any(Handler.class),
+ eq(Activity.RESULT_OK),
+ anyString(),
+ any(Bundle.class));
+
+ // But that we did send a regular broadcast.
+ ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext).sendBroadcastAsUser(
+ intentArgumentCaptor.capture(),
+ eq(UserHandle.CURRENT),
+ eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS),
+ eq(AppOpsManager.OP_PROCESS_OUTGOING_CALLS));
+ Intent capturedIntent = intentArgumentCaptor.getValue();
+ assertEquals(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, capturedIntent.getFlags());
+ }
+
private ReceiverIntentPair regularCallTestHelper(Intent intent,
Bundle expectedAdditionalExtras) {
Uri handle = intent.getData();
@@ -542,7 +625,7 @@
boolean isDefaultPhoneApp) {
NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster(
mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
- isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils);
+ isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
NewOutgoingCallIntentBroadcaster.CallDisposition cd = b.evaluateCall();
if (cd.disconnectCause == DisconnectCause.NOT_DISCONNECTED) {
b.processCall(mCall, cd);
diff --git a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
index fed8084..8eefd96 100644
--- a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
@@ -13,11 +13,13 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.telecom.Connection;
import android.telecom.ParcelableCall;
import android.telecom.PhoneAccountHandle;
import android.telephony.ims.ImsCallProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallerInfoLookupHelper;
@@ -57,6 +59,7 @@
when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(mPhoneAccountRegistrar);
+ when(mCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(any())).thenReturn(null);
when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
.thenReturn(false);
@@ -75,7 +78,8 @@
false /* shouldAttachToExistingConnection */,
false /* isConference */,
mClockProxy /* ClockProxy */,
- mToastProxy);
+ mToastProxy,
+ mFeatureFlags);
}
@Override
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index e573bb8..a480a7b 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -23,14 +23,15 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -57,13 +58,14 @@
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.test.suitebuilder.annotation.MediumTest;
import android.util.Xml;
import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.util.FastXmlSerializer;
import com.android.server.telecom.AppLabelProxy;
import com.android.server.telecom.DefaultDialerCache;
@@ -92,6 +94,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -110,13 +113,15 @@
private final String PACKAGE_1 = "PACKAGE_1";
private final String PACKAGE_2 = "PACKAGE_2";
private final String COMPONENT_NAME = "com.android.server.telecom.tests.MockConnectionService";
- private final UserHandle USER_HANDLE_10 = new UserHandle(10);
+ private final UserHandle USER_HANDLE_10 = UserHandle.of(10);
+ private final UserHandle USER_HANDLE_1000 = UserHandle.of(1000);
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
private PhoneAccountRegistrar mRegistrar;
@Mock private SubscriptionManager mSubscriptionManager;
@Mock private TelecomManager mTelecomManager;
@Mock private DefaultDialerCache mDefaultDialerCache;
@Mock private AppLabelProxy mAppLabelProxy;
+ @Mock private FeatureFlags mTelephonyFeatureFlags;
@Override
@Before
@@ -131,11 +136,15 @@
.delete();
when(mDefaultDialerCache.getDefaultDialerApplication(anyInt()))
.thenReturn("com.android.dialer");
- when(mAppLabelProxy.getAppLabel(anyString()))
+ when(mAppLabelProxy.getAppLabel(anyString(), any()))
.thenReturn(TEST_LABEL);
mRegistrar = new PhoneAccountRegistrar(
- mComponentContextFixture.getTestDouble().getApplicationContext(),
- mLock, FILE_NAME, mDefaultDialerCache, mAppLabelProxy);
+ mComponentContextFixture.getTestDouble().getApplicationContext(), mLock, FILE_NAME,
+ mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags, mFeatureFlags);
+ mRegistrar.setCurrentUserHandle(UserHandle.SYSTEM);
+ when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
+ when(mFeatureFlags.unregisterUnresolvableAccounts()).thenReturn(true);
+ when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
}
@Override
@@ -154,12 +163,14 @@
public void testPhoneAccountHandle() throws Exception {
PhoneAccountHandle input = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id0");
PhoneAccountHandle result = roundTripXml(this, input,
- PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext);
+ 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);
+ PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+ mTelephonyFeatureFlags, mFeatureFlags);
Log.i(this, "inputN = %s, resultN = %s", inputN, resultN);
assertPhoneAccountHandleEquals(inputN, resultN);
}
@@ -182,7 +193,112 @@
.setIsEnabled(true)
.build();
PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
- mContext);
+ mContext, mTelephonyFeatureFlags, mFeatureFlags);
+
+ assertPhoneAccountEquals(input, result);
+ }
+
+ @MediumTest
+ @Test
+ public void testPhoneAccountParsing_simultaneousCallingRestriction() throws Exception {
+ 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 = 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();
+ testBundle.putInt("EXTRA_INT_1", 1);
+ testBundle.putInt("EXTRA_INT_100", 100);
+ testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+ testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+ testBundle.putString("EXTRA_STR1", "Hello");
+ testBundle.putString("EXTRA_STR2", "There");
+
+ Set<PhoneAccountHandle> restriction = new HashSet<>(10);
+ for (int i = 0; i < 10; i++) {
+ restriction.add(makeQuickAccountHandleForUser("id" + i, USER_HANDLE_10));
+ }
+
+ PhoneAccount input = makeQuickAccountBuilder("id0", 0, USER_HANDLE_10)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+ .setExtras(testBundle)
+ .setIsEnabled(true)
+ .setSimultaneousCallingRestriction(restriction)
+ .build();
+ PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
+ mContext, mTelephonyFeatureFlags, mFeatureFlags);
+
+ assertPhoneAccountEquals(input, result);
+ }
+
+ @MediumTest
+ @Test
+ public void testPhoneAccountParsing_simultaneousCallingRestrictionOnOffFlag() throws Exception {
+ // Start the test with the flag on
+ 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 = 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();
+ testBundle.putInt("EXTRA_INT_1", 1);
+ testBundle.putInt("EXTRA_INT_100", 100);
+ testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+ testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+ testBundle.putString("EXTRA_STR1", "Hello");
+ testBundle.putString("EXTRA_STR2", "There");
+
+ Set<PhoneAccountHandle> restriction = new HashSet<>(10);
+ for (int i = 0; i < 10; i++) {
+ restriction.add(makeQuickAccountHandleForUser("id" + i, USER_HANDLE_10));
+ }
+
+ PhoneAccount input = makeQuickAccountBuilder("id0", 0, USER_HANDLE_10)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+ .setExtras(testBundle)
+ .setIsEnabled(true)
+ .setSimultaneousCallingRestriction(restriction)
+ .build();
+ byte[] xmlData = toXml(input, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+ mTelephonyFeatureFlags);
+ // Simulate turning off the flag after reboot
+ doReturn(false).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+ mTelephonyFeatureFlags, mFeatureFlags);
+
+ assertNotNull(result);
+ assertFalse(result.hasSimultaneousCallingRestriction());
+ }
+
+ @MediumTest
+ @Test
+ public void testPhoneAccountParsing_simultaneousCallingRestrictionOffOnFlag() throws Exception {
+ // Start the test with the flag on
+ doReturn(false).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ Bundle testBundle = new Bundle();
+ testBundle.putInt("EXTRA_INT_1", 1);
+ testBundle.putInt("EXTRA_INT_100", 100);
+ testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+ testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+ testBundle.putString("EXTRA_STR1", "Hello");
+ testBundle.putString("EXTRA_STR2", "There");
+
+ PhoneAccount input = makeQuickAccountBuilder("id0", 0, USER_HANDLE_10)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+ .setExtras(testBundle)
+ .setIsEnabled(true)
+ .build();
+ byte[] xmlData = toXml(input, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+ mTelephonyFeatureFlags);
+ // Simulate turning on the flag after reboot
+ doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+ mTelephonyFeatureFlags, mFeatureFlags);
assertPhoneAccountEquals(input, result);
}
@@ -254,12 +370,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.sDefaultPhoneAcountHandleXml, mContext);
+ PhoneAccountRegistrar.sDefaultPhoneAccountHandleXml, mContext,
+ mTelephonyFeatureFlags, mFeatureFlags);
assertDefaultPhoneAccountHandleEquals(input, result);
}
@@ -289,7 +407,7 @@
.setExtras(testBundle)
.build();
PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
- mContext);
+ mContext, mTelephonyFeatureFlags, mFeatureFlags);
Bundle extras = result.getExtras();
assertFalse(extras.keySet().contains("EXTRA_STR2"));
@@ -303,8 +421,7 @@
public void testState() throws Exception {
PhoneAccountRegistrar.State input = makeQuickState();
PhoneAccountRegistrar.State result = roundTripXml(this, input,
- PhoneAccountRegistrar.sStateXml,
- mContext);
+ PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags, mFeatureFlags);
assertStateEquals(input, result);
}
@@ -353,6 +470,60 @@
PhoneAccount.SCHEME_TEL));
}
+ /**
+ * Verify when a {@link android.telecom.ConnectionService} is disabled or cannot be resolved,
+ * all phone accounts are unregistered when calling
+ * {@link PhoneAccountRegistrar#cleanupAndGetVerifiedAccounts(PhoneAccount)}.
+ */
+ @Test
+ public void testCannotResolveServiceUnregistersAccounts() throws Exception {
+ ComponentName componentName = makeQuickConnectionServiceComponentName();
+ PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+ // add the ConnectionService and register a single phone account for it
+ mComponentContextFixture.addConnectionService(componentName,
+ Mockito.mock(IConnectionService.class));
+ registerAndEnableAccount(account);
+ // verify the start state
+ assertEquals(1,
+ mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+ USER_HANDLE_10).size());
+ // remove the ConnectionService so that the account cannot be resolved anymore
+ mComponentContextFixture.removeConnectionService(componentName,
+ Mockito.mock(IConnectionService.class));
+ // verify the account is unregistered when fetching the phone accounts for the package
+ assertEquals(1,
+ mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+ USER_HANDLE_10).size());
+ assertEquals(0,
+ mRegistrar.cleanupAndGetVerifiedAccounts(account).size());
+ assertEquals(0,
+ mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+ USER_HANDLE_10).size());
+ }
+
+ /**
+ * Verify that if a client adds both the {@link
+ * PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} capability AND is backed by a
+ * {@link android.telecom.ConnectionService}, a {@link IllegalArgumentException} is thrown.
+ */
+ @Test
+ public void testConnectionServiceAndTransactionalAccount() throws Exception {
+ PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED
+ | PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+ mComponentContextFixture.addConnectionService(
+ makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+ try {
+ registerAndEnableAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // test passed, ignore Exception.
+ }
+ }
+
@MediumTest
@Test
public void testSimCallManager() throws Exception {
@@ -1135,9 +1306,9 @@
// 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));
+ List<UserHandle> users = Arrays.asList(UserHandle.SYSTEM, USER_HANDLE_1000);
PhoneAccount pa1 = new PhoneAccount.Builder(
new PhoneAccountHandle(new ComponentName(PACKAGE_1, COMPONENT_NAME), "1234",
@@ -1161,10 +1332,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
@@ -1436,7 +1607,7 @@
.setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
// WHEN
- when(mAppLabelProxy.getAppLabel(anyString())).thenReturn(invalidLabel);
+ when(mAppLabelProxy.getAppLabel(anyString(), any())).thenReturn(invalidLabel);
// THEN
try {
@@ -1624,6 +1795,107 @@
}
/**
+ * Ensure an IllegalArgumentException is thrown when adding too many PhoneAccountHandles to
+ * a PhoneAccount.
+ */
+ @Test
+ public void testLimitOnSimultaneousCallingRestriction_tooManyElements() throws Exception {
+ doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+ Set<PhoneAccountHandle> tooManyElements = new HashSet<>(11);
+ for (int i = 0; i < 11; i++) {
+ tooManyElements.add(makeQuickAccountHandle(TEST_ID + i));
+ }
+ PhoneAccount tooManyRestrictionsPA = new PhoneAccount.Builder(
+ makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+ .setSimultaneousCallingRestriction(tooManyElements)
+ .build();
+ try {
+ mRegistrar.registerPhoneAccount(tooManyRestrictionsPA);
+ fail("should have hit registrations exception in "
+ + "enforceSimultaneousCallingRestrictionLimit");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when adding a PhoneAccountHandle where the
+ * package name field is too large.
+ */
+ @Test
+ public void testLimitOnSimultaneousCallingRestriction_InvalidPackageName() throws Exception {
+ doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+ Set<PhoneAccountHandle> invalidElement = new HashSet<>(1);
+ invalidElement.add(new PhoneAccountHandle(new ComponentName(INVALID_STR, "Class"),
+ TEST_ID));
+ PhoneAccount invalidRestrictionPA = new PhoneAccount.Builder(
+ makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+ .setSimultaneousCallingRestriction(invalidElement)
+ .build();
+ try {
+ mRegistrar.registerPhoneAccount(invalidRestrictionPA);
+ fail("should have hit package name size limit exception in "
+ + "enforceSimultaneousCallingRestrictionLimit");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when adding a PhoneAccountHandle where the
+ * class name field is too large.
+ */
+ @Test
+ public void testLimitOnSimultaneousCallingRestriction_InvalidClassName() throws Exception {
+ doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+ Set<PhoneAccountHandle> invalidElement = new HashSet<>(1);
+ invalidElement.add(new PhoneAccountHandle(new ComponentName("pkg", INVALID_STR),
+ TEST_ID));
+ PhoneAccount invalidRestrictionPA = new PhoneAccount.Builder(
+ makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+ .setSimultaneousCallingRestriction(invalidElement)
+ .build();
+ try {
+ mRegistrar.registerPhoneAccount(invalidRestrictionPA);
+ fail("should have hit class name size limit exception in "
+ + "enforceSimultaneousCallingRestrictionLimit");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when adding a PhoneAccountHandle where the
+ * ID field is too large.
+ */
+ @Test
+ public void testLimitOnSimultaneousCallingRestriction_InvalidIdSize() throws Exception {
+ doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+ Set<PhoneAccountHandle> invalidIdElement = new HashSet<>(1);
+ invalidIdElement.add(new PhoneAccountHandle(makeQuickConnectionServiceComponentName(),
+ INVALID_STR));
+ PhoneAccount invalidRestrictionPA = new PhoneAccount.Builder(
+ makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+ .setSimultaneousCallingRestriction(invalidIdElement)
+ .build();
+ try {
+ mRegistrar.registerPhoneAccount(invalidRestrictionPA);
+ fail("should have hit ID size limit exception in "
+ + "enforceSimultaneousCallingRestrictionLimit");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
* Ensure an IllegalArgumentException is thrown when adding an address over the limit
*/
@Test
@@ -1654,7 +1926,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());
@@ -1700,6 +1972,86 @@
}
}
+ @Test
+ public void testGetPhoneAccountAcrossUsers() throws Exception {
+ when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(true);
+ mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+ Mockito.mock(IConnectionService.class));
+
+ PhoneAccount accountForCurrent = makeQuickAccountBuilder("id_0", 0, UserHandle.CURRENT)
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+ PhoneAccount accountForAll = makeQuickAccountBuilder("id_0", 0, UserHandle.ALL)
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_MULTI_USER).build();
+ PhoneAccount accountForWorkProfile = makeQuickAccountBuilder("id_1", 1, USER_HANDLE_10)
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+
+ registerAndEnableAccount(accountForCurrent);
+ registerAndEnableAccount(accountForAll);
+ registerAndEnableAccount(accountForWorkProfile);
+
+ List<PhoneAccount> accountsForUser = mRegistrar.getPhoneAccounts(0, 0,
+ null, null, false, USER_HANDLE_10, false, false);
+ List<PhoneAccount> accountsVisibleUser = mRegistrar.getPhoneAccounts(0, 0,
+ null, null, false, USER_HANDLE_10, false, true);
+ List<PhoneAccount> accountsAcrossUser = mRegistrar.getPhoneAccounts(0, 0,
+ null, null, false, USER_HANDLE_10, true, false);
+
+ // Return the account exactly matching the user if it exists
+ assertEquals(1, accountsForUser.size());
+ assertTrue(accountsForUser.contains(accountForWorkProfile));
+ // The accounts visible to the user without across user permission
+ assertEquals(2, accountsVisibleUser.size());
+ assertTrue(accountsVisibleUser.containsAll(accountsForUser));
+ assertTrue(accountsVisibleUser.contains(accountForAll));
+ // The accounts visible to the user with across user permission
+ assertEquals(3, accountsAcrossUser.size());
+ assertTrue(accountsAcrossUser.containsAll(accountsVisibleUser));
+ assertTrue(accountsAcrossUser.contains(accountForCurrent));
+
+ mRegistrar.unregisterPhoneAccount(accountForWorkProfile.getAccountHandle());
+
+ accountsForUser = mRegistrar.getPhoneAccounts(0, 0,
+ null, null, false, USER_HANDLE_10, false, false);
+
+ // Return the account visible for the user if no account exactly matches the user
+ assertEquals(1, accountsForUser.size());
+ 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);
@@ -1818,35 +2170,44 @@
Object self,
T input,
PhoneAccountRegistrar.XmlSerialization<T> xml,
- Context context)
+ Context context,
+ FeatureFlags telephonyFeatureFlags,
+ com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
throws Exception {
Log.d(self, "Input = %s", input);
- byte[] data;
- {
- XmlSerializer serializer = new FastXmlSerializer();
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
- xml.writeToXml(input, serializer, context);
- serializer.flush();
- data = baos.toByteArray();
- }
+ byte[] data = toXml(input, xml, context, telephonyFeatureFlags);
Log.i(self, "====== XML data ======\n%s", new String(data));
- T result = null;
- {
- XmlPullParser parser = Xml.newPullParser();
- parser.setInput(new BufferedInputStream(new ByteArrayInputStream(data)), null);
- parser.nextTag();
- result = xml.readFromXml(parser, MAX_VERSION, context);
- }
+ T result = fromXml(data, xml, context, telephonyFeatureFlags, telecomFeatureFlags);
Log.i(self, "result = " + result);
return result;
}
+ private static <T> byte[] toXml(T input, PhoneAccountRegistrar.XmlSerialization<T> xml,
+ Context context, FeatureFlags telephonyFeatureFlags) throws Exception {
+ XmlSerializer serializer = new FastXmlSerializer();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+ xml.writeToXml(input, serializer, context, telephonyFeatureFlags);
+ serializer.flush();
+ return baos.toByteArray();
+ }
+
+ private static <T> T fromXml(byte[] data, PhoneAccountRegistrar.XmlSerialization<T> xml,
+ 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, telecomFeatureFlags);
+
+ }
+
private static void assertPhoneAccountHandleEquals(PhoneAccountHandle a, PhoneAccountHandle b) {
if (a != b) {
assertEquals(
@@ -1895,6 +2256,12 @@
assertEquals(a.getSupportedUriSchemes(), b.getSupportedUriSchemes());
assertBundlesEqual(a.getExtras(), b.getExtras());
assertEquals(a.isEnabled(), b.isEnabled());
+ assertEquals(a.hasSimultaneousCallingRestriction(),
+ b.hasSimultaneousCallingRestriction());
+ if (a.hasSimultaneousCallingRestriction()) {
+ assertEquals(a.getSimultaneousCallingRestriction(),
+ b.getSimultaneousCallingRestriction());
+ }
} else {
fail("Phone accounts not equal: " + a + ", " + b);
}
@@ -1935,6 +2302,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));
@@ -1943,9 +2311,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,
@@ -1954,6 +2322,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));
@@ -1961,9 +2330,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/ProximitySensorManagerTest.java b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
index 807b7cf..310f4cb 100644
--- a/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
@@ -16,8 +16,14 @@
package com.android.server.telecom.tests;
+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.os.PowerManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
@@ -33,11 +39,6 @@
import java.util.List;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class ProximitySensorManagerTest extends TelecomTestCase{
diff --git a/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
new file mode 100644
index 0000000..e8d39c8
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.InCallTonePlayer;
+import com.android.server.telecom.RingbackPlayer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(JUnit4.class)
+public class RingbackPlayerTest extends TelecomTestCase {
+ @Mock InCallTonePlayer.Factory mFactory;
+ @Mock Call mCall;
+ @Mock InCallTonePlayer mTonePlayer;
+
+ private RingbackPlayer mRingbackPlayer;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ when(mFactory.createPlayer(any(Call.class), anyInt())).thenReturn(mTonePlayer);
+ mRingbackPlayer = new RingbackPlayer(mFactory);
+ }
+
+ @SmallTest
+ @Test
+ public void testPlayerSync() {
+ // make sure InCallTonePlayer try to start playing the tone after RingbackPlayer receives
+ // stop tone request.
+ CountDownLatch latch = new CountDownLatch(1);
+ doReturn(CallState.DIALING).when(mCall).getState();
+ doAnswer(x -> {
+ new Thread(() -> {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }).start();
+ return true;
+ }).when(mTonePlayer).startTone();
+
+ mRingbackPlayer.startRingbackForCall(mCall);
+ mRingbackPlayer.stopRingbackForCall(mCall);
+ assertFalse(mRingbackPlayer.isRingbackPlaying());
+ latch.countDown();
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 34360ca..c4d9678 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -16,8 +16,10 @@
package com.android.server.telecom.tests;
+import static android.os.VibrationEffect.EFFECT_CLICK;
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +32,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -44,6 +47,7 @@
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.VolumeShaper;
+import android.media.audio.Flags;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
@@ -51,9 +55,16 @@
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.os.VibratorInfo;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.AsyncRingtonePlayer;
import com.android.server.telecom.Call;
@@ -63,33 +74,55 @@
import com.android.server.telecom.Ringer;
import com.android.server.telecom.RingtoneFactory;
import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Spy;
+import java.time.Duration;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
@RunWith(JUnit4.class)
public class RingerTest extends TelecomTestCase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final Uri FAKE_RINGTONE_URI = Uri.parse("content://media/fake/audio/1729");
+
+ private static final Uri FAKE_VIBRATION_URI = Uri.parse("file://media/fake/vibration/1729");
+
+ private static final String VIBRATION_PARAM = "vibration_uri";
// Returned when the a URI-based VibrationEffect is attempted, to avoid depending on actual
// device configuration for ringtone URIs. The actual Uri can be verified via the
// VibrationEffectProxy mock invocation.
private static final VibrationEffect URI_VIBRATION_EFFECT =
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+ private static final VibrationEffect EXPECTED_SIMPLE_VIBRATION_PATTERN =
+ VibrationEffect.createWaveform(
+ new long[] {0, 1000, 1000}, new int[] {0, 255, 0}, 1);
+ private static final VibrationEffect EXPECTED_PULSE_VIBRATION_PATTERN =
+ VibrationEffect.createWaveform(
+ Ringer.PULSE_PATTERN, Ringer.PULSE_AMPLITUDE, 5);
@Mock InCallTonePlayer.Factory mockPlayerFactory;
@Mock SystemSettingsUtil mockSystemSettingsUtil;
@Mock RingtoneFactory mockRingtoneFactory;
@Mock Vibrator mockVibrator;
+ @Mock VibratorInfo mockVibratorInfo;
@Mock InCallController mockInCallController;
@Mock NotificationManager mockNotificationManager;
@Mock Ringer.AccessibilityManagerAdapter mockAccessibilityManagerAdapter;
+ @Mock private FeatureFlags mFeatureFlags;
@Spy Ringer.VibrationEffectProxy spyVibrationEffectProxy;
@@ -111,21 +144,25 @@
@Before
public void setUp() throws Exception {
super.setUp();
- mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext());
+ when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+ when(mFeatureFlags.ensureInCarRinging()).thenReturn(false);
doReturn(URI_VIBRATION_EFFECT).when(spyVibrationEffectProxy).get(any(), any());
- when(mockPlayerFactory.createPlayer(anyInt())).thenReturn(mockTonePlayer);
+ when(mockPlayerFactory.createPlayer(any(Call.class), anyInt())).thenReturn(mockTonePlayer);
mockAudioManager = mContext.getSystemService(AudioManager.class);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+ when(mockVibrator.getInfo()).thenReturn(mockVibratorInfo);
when(mockSystemSettingsUtil.isHapticPlaybackSupported(any(Context.class)))
.thenAnswer((invocation) -> mIsHapticPlaybackSupported);
mockNotificationManager =mContext.getSystemService(NotificationManager.class);
when(mockTonePlayer.startTone()).thenReturn(true);
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
- when(mockRingtoneFactory.hasHapticChannels(any(Ringtone.class))).thenReturn(false);
+ when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
when(mockCall1.getState()).thenReturn(CallState.RINGING);
when(mockCall2.getState()).thenReturn(CallState.RINGING);
when(mockCall1.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
when(mockCall2.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
+ when(mockCall1.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+ when(mockCall2.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
// Set BT active state in tests to ensure that we do not end up blocking tests for 1 sec
// waiting for BT to connect in unit tests by default.
asyncRingtonePlayer.updateBtActiveState(true);
@@ -140,7 +177,8 @@
private void createRingerUnderTest() {
mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil,
asyncRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy,
- mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter);
+ mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter,
+ mFeatureFlags);
// This future is used to wait for AsyncRingtonePlayer to finish its part.
mRingerUnderTest.setBlockOnRingingFuture(mRingCompletionFuture);
}
@@ -153,15 +191,147 @@
@SmallTest
@Test
- public void testNoActionInTheaterMode() throws Exception {
- // Start call waiting to make sure that it doesn't stop when we start ringing
- mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockSystemSettingsUtil.isTheaterModeOn(any(Context.class))).thenReturn(true);
- assertFalse(startRingingAndWaitForAsync(mockCall2, false));
- verifyZeroInteractions(mockRingtoneFactory);
- verify(mockTonePlayer, never()).stopTone();
- verify(mockVibrator, never())
- .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+ public void testSimpleVibrationPrecedesValidSupportedDefaultRingVibrationOverride()
+ throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+ mockVibrationResourceValues(
+ """
+ <vibration-effect>
+ <predefined-effect name="click"/>
+ </vibration-effect>
+ """,
+ /* useSimpleVibration= */ true);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+ createRingerUnderTest();
+
+ assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+ }
+
+ @SmallTest
+ @Test
+ public void testDefaultRingVibrationOverrideNotUsedWhenFeatureIsDisabled()
+ throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(false);
+ mockVibrationResourceValues(
+ """
+ <vibration-effect>
+ <waveform-effect>
+ <waveform-entry durationMs="100" amplitude="0"/>
+ <repeating>
+ <waveform-entry durationMs="500" amplitude="default"/>
+ <waveform-entry durationMs="700" amplitude="0"/>
+ </repeating>
+ </waveform-effect>
+ </vibration-effect>
+ """,
+ /* useSimpleVibration= */ false);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+ createRingerUnderTest();
+
+ assertEquals(EXPECTED_PULSE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+ }
+
+ @SmallTest
+ @Test
+ public void testValidSupportedRepeatingDefaultRingVibrationOverride() throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+ mockVibrationResourceValues(
+ """
+ <vibration-effect>
+ <waveform-effect>
+ <waveform-entry durationMs="100" amplitude="0"/>
+ <repeating>
+ <waveform-entry durationMs="500" amplitude="default"/>
+ <waveform-entry durationMs="700" amplitude="0"/>
+ </repeating>
+ </waveform-effect>
+ </vibration-effect>
+ """,
+ /* useSimpleVibration= */ false);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+ createRingerUnderTest();
+
+ assertEquals(
+ VibrationEffect.createWaveform(new long[]{100, 500, 700}, /* repeat= */ 1),
+ mRingerUnderTest.mDefaultVibrationEffect);
+ }
+
+ @SmallTest
+ @Test
+ public void testValidSupportedNonRepeatingDefaultRingVibrationOverride() throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+ mockVibrationResourceValues(
+ """
+ <vibration-effect>
+ <predefined-effect name="click"/>
+ </vibration-effect>
+ """,
+ /* useSimpleVibration= */ false);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+ createRingerUnderTest();
+
+ assertEquals(
+ VibrationEffect
+ .startComposition()
+ .repeatEffectIndefinitely(
+ VibrationEffect
+ .startComposition()
+ .addEffect(VibrationEffect.createPredefined(EFFECT_CLICK))
+ .addOffDuration(Duration.ofSeconds(1))
+ .compose()
+ )
+ .compose(),
+ mRingerUnderTest.mDefaultVibrationEffect);
+ }
+
+ @SmallTest
+ @Test
+ public void testValidButUnsupportedDefaultRingVibrationOverride() throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+ mockVibrationResourceValues(
+ """
+ <vibration-effect>
+ <predefined-effect name="click"/>
+ </vibration-effect>
+ """,
+ /* useSimpleVibration= */ false);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(
+ eq(VibrationEffect.createPredefined(EFFECT_CLICK)))).thenReturn(false);
+
+ createRingerUnderTest();
+
+ assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+ }
+
+ @SmallTest
+ @Test
+ public void testInvalidDefaultRingVibrationOverride() throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+ mockVibrationResourceValues(
+ /* defaultVibrationContent= */ "bad serialization",
+ /* useSimpleVibration= */ false);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+ createRingerUnderTest();
+
+ assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+ }
+
+ @SmallTest
+ @Test
+ public void testEmptyDefaultRingVibrationOverride() throws Exception {
+ when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+ mockVibrationResourceValues(
+ /* defaultVibrationContent= */ "", /* useSimpleVibration= */ false);
+ when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+ createRingerUnderTest();
+
+ assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
}
@SmallTest
@@ -231,7 +401,7 @@
@SmallTest
@Test
public void testCallWaitingButNoRingForSpecificContacts() throws Exception {
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+ when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
// Start call waiting to make sure that it does stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
verify(mockTonePlayer).startTone();
@@ -255,7 +425,7 @@
// Pretend we're using audio coupled haptics.
setIsUsingHaptics(mockRingtone, true);
assertTrue(startRingingAndWaitForAsync(mockCall1, false));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -266,6 +436,62 @@
@SmallTest
@Test
+ public void testAudibleRingWhenNotificationSoundShouldPlay() throws Exception {
+ when(mFeatureFlags.ensureInCarRinging()).thenReturn(true);
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
+ mRingerUnderTest.startCallWaiting(mockCall1);
+ AudioAttributes aa = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();
+ // Set AudioManager#shouldNotificationSoundPlay to true:
+ when(mockAudioManager.shouldNotificationSoundPlay(aa)).thenReturn(true);
+ enableVibrationWhenRinging();
+
+ // This will set AudioManager#getStreamVolume to 0. This test ensures that whether a
+ // ringtone is audible is controlled by AudioManager#shouldNotificationSoundPlay instead:
+ ensureRingerIsNotAudible();
+
+ // Ensure an audible ringtone is played:
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockTonePlayer).stopTone();
+ verify(mockRingtoneFactory, atLeastOnce()).getRingtone(any(Call.class),
+ nullable(VolumeShaper.Configuration.class), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
+ verify(mockRingtone).play();
+
+ // Ensure a vibration plays:
+ verify(mockVibrator).vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+ }
+
+ @SmallTest
+ @Test
+ public void testNoAudibleRingWhenNotificationSoundShouldNotPlay() throws Exception {
+ when(mFeatureFlags.ensureInCarRinging()).thenReturn(true);
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
+ mRingerUnderTest.startCallWaiting(mockCall1);
+ AudioAttributes aa = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();
+ // Set AudioManager#shouldNotificationSoundPlay to false:
+ when(mockAudioManager.shouldNotificationSoundPlay(aa)).thenReturn(false);
+ enableVibrationWhenRinging();
+
+ // This will set AudioManager#getStreamVolume to 100. This test ensures that whether a
+ // ringtone is audible is controlled by AudioManager#shouldNotificationSoundPlay instead:
+ ensureRingerIsAudible();
+
+ // Ensure no audible ringtone is played:
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockTonePlayer).stopTone();
+
+ // Ensure a vibration plays:
+ verify(mockVibrator).vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+ }
+
+ @SmallTest
+ @Test
public void testVibrateButNoRingForNullRingtone() throws Exception {
when(mockRingtoneFactory.getRingtone(
any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
@@ -297,16 +523,12 @@
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockRingtoneFactory.getRingtone(any(Call.class), eq(null), anyBoolean()))
- .thenReturn(mockRingtone);
+ .thenReturn(new Pair(FAKE_RINGTONE_URI, mockRingtone));
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
enableVibrationWhenRinging();
assertFalse(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
- // Try to play a silent haptics ringtone
- verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
- verifyNoMoreInteractions(mockRingtoneFactory);
- verify(mockRingtone).play();
// Play default vibration when future completes with no audio coupled haptics
verify(mockVibrator).vibrate(eq(mRingerUnderTest.mDefaultVibrationEffect),
@@ -333,28 +555,6 @@
@SmallTest
@Test
- public void testAudioCoupledHapticsForSilentRingtone() throws Exception {
- Ringtone mockRingtone = ensureRingtoneMocked();
-
- mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
- when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
- setIsUsingHaptics(mockRingtone, true);
- enableVibrationWhenRinging();
- assertFalse(startRingingAndWaitForAsync(mockCall2, false));
-
- verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
- verifyNoMoreInteractions(mockRingtoneFactory);
- verify(mockTonePlayer).stopTone();
- // Try to play a silent haptics ringtone
- verify(mockRingtone).play();
- // Skip vibration for audio coupled haptics
- verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
- any(VibrationAttributes.class));
- }
-
- @SmallTest
- @Test
public void testCustomVibrationForRingtone() throws Exception {
mRingerUnderTest.startCallWaiting(mockCall1);
Ringtone mockRingtone = ensureRingtoneMocked();
@@ -363,7 +563,7 @@
enableVibrationWhenRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), isNull(), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockRingtone).play();
@@ -380,7 +580,7 @@
ensureRingerIsAudible();
enableVibrationOnlyWhenNotRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -399,7 +599,7 @@
enableRampingRinger();
enableVibrationWhenRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -431,7 +631,7 @@
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(100);
enableVibrationWhenRinging();
assertTrue(startRingingAndWaitForAsync(mockCall2, true));
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), isNull(), anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
@@ -452,7 +652,7 @@
asyncRingtonePlayer.updateBtActiveState(true);
mRingCompletionFuture.get();
- verify(mockRingtoneFactory, times(1))
+ verify(mockRingtoneFactory, atLeastOnce())
.getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
anyBoolean());
verifyNoMoreInteractions(mockRingtoneFactory);
@@ -492,7 +692,7 @@
when(mContext.getSystemService(NotificationManager.class)).thenReturn(
mockNotificationManager);
// suppress the call
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+ when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
// run the method under test
assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
@@ -501,7 +701,7 @@
// verify we never set the call object and matchesCallFilter is called
verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
verify(mockNotificationManager, times(1))
- .matchesCallFilter(any(Bundle.class));
+ .matchesCallFilter(any(Uri.class));
}
/**
@@ -514,7 +714,6 @@
when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
// alert the user of the call
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
// run the method under test
assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
@@ -523,7 +722,7 @@
// verify we never set the call object and matchesCallFilter is called
verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
verify(mockNotificationManager, times(1))
- .matchesCallFilter(any(Bundle.class));
+ .matchesCallFilter(any(Uri.class));
}
/**
@@ -539,7 +738,7 @@
// THEN
assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
- verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
+ verify(mockNotificationManager, never()).matchesCallFilter(any(Uri.class));
}
@Test
@@ -549,7 +748,7 @@
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+ when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
assertFalse(mRingerUnderTest.shouldRingForContact(mockCall2));
assertFalse(startRingingAndWaitForAsync(mockCall2, false));
@@ -564,7 +763,6 @@
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2));
assertTrue(startRingingAndWaitForAsync(mockCall2, false));
@@ -584,14 +782,13 @@
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
- return mockRingtone;
+ return new Pair(FAKE_RINGTONE_URI, mockRingtone);
});
// Start call waiting to make sure that it doesn't stop when we start ringing
enableVibrationWhenRinging();
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
- when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2));
assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
@@ -618,6 +815,37 @@
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
+ @SmallTest
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
+ public void testNoVibrateForSilentRingtoneIfRingtoneHasVibration() throws Exception {
+ Uri FAKE_RINGTONE_VIBRATION_URI =
+ FAKE_RINGTONE_URI.buildUpon().appendQueryParameter(
+ VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
+ Ringtone mockRingtone = mock(Ringtone.class);
+ Pair<Uri, Ringtone> ringtoneInfo = new Pair(FAKE_RINGTONE_VIBRATION_URI, mockRingtone);
+ when(mockRingtoneFactory.getRingtone(
+ any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
+ .thenReturn(ringtoneInfo);
+ mComponentContextFixture.putBooleanResource(
+ com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
+ createRingerUnderTest(); // Needed after mock the config.
+
+ mRingerUnderTest.startCallWaiting(mockCall1);
+ when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+ when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+ enableVibrationWhenRinging();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+
+ verify(mockRingtoneFactory, atLeastOnce())
+ .getRingtone(any(Call.class), eq(null), eq(false));
+ verifyNoMoreInteractions(mockRingtoneFactory);
+ verify(mockTonePlayer).stopTone();
+ // Skip vibration play in Ringer if a vibration was specified to the ringtone
+ verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
+ any(VibrationAttributes.class));
+ }
+
/**
* Call startRinging and wait for its effects to have played out, to allow reliable assertions
* after it. The effects are generally "start playing ringtone" and "start vibration" - not
@@ -664,10 +892,20 @@
private Ringtone ensureRingtoneMocked() {
Ringtone mockRingtone = mock(Ringtone.class);
+ Pair<Uri, Ringtone> ringtoneInfo = new Pair(
+ FAKE_RINGTONE_URI, mockRingtone);
when(mockRingtoneFactory.getRingtone(
any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
- .thenReturn(mockRingtone);
- when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(mockRingtone);
+ .thenReturn(ringtoneInfo);
return mockRingtone;
}
+
+ private void mockVibrationResourceValues(
+ String defaultVibrationContent, boolean useSimpleVibration) {
+ mComponentContextFixture.putRawResource(
+ com.android.internal.R.raw.default_ringtone_vibration_effect,
+ defaultVibrationContent);
+ mComponentContextFixture.putBooleanResource(
+ R.bool.use_simple_vibration_pattern, useSimpleVibration);
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/SessionManagerTest.java b/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
index cf84b7c..631d522 100644
--- a/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
@@ -21,10 +21,15 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import android.telecom.Log;
import android.telecom.Logging.Session;
import android.telecom.Logging.SessionManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.flags.Flags;
+
+import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
@@ -56,13 +61,11 @@
@Before
public void setUp() throws Exception {
super.setUp();
- mTestSessionManager = new SessionManager();
+ mTestSessionManager = new SessionManager(null);
mTestSessionManager.registerSessionListener(((sessionName, timeMs) -> {
mfullSessionCompleteTime = timeMs;
mFullSessionMethodName = sessionName;
}));
- // Remove automatic stale session cleanup for testing
- mTestSessionManager.mCleanStaleSessions = null;
}
@Override
@@ -410,4 +413,33 @@
assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
assertNull(sessionRef.get());
}
+
+ /**
+ * If Telecom gets into a situation where there are MANY sub-sessions created in a deep tree,
+ * ensure that cleanup still happens properly.
+ */
+ @SmallTest
+ @Test
+ public void testManySubsessionCleanupStress() {
+ // This test will mostly likely fail with recursion due to stack overflow
+ if (!Flags.endSessionImprovements()) return;
+ Log.setIsExtendedLoggingEnabled(false);
+ mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
+ mTestSessionManager.startSession(TEST_PARENT_NAME, null);
+ Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
+ Session subsession;
+ try {
+ for (int i = 0; i < 10000; i++) {
+ subsession = mTestSessionManager.createSubsession();
+ mTestSessionManager.endSession();
+ mTestSessionManager.continueSession(subsession, TEST_CHILD_NAME + i);
+ }
+ mTestSessionManager.endSession();
+ } catch (Exception e) {
+ fail("Exception: " + e);
+ }
+ assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
+ assertTrue(parentSession.isSessionCompleted());
+ assertTrue(parentSession.getChildSessions().isEmpty());
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/SessionTest.java b/tests/src/com/android/server/telecom/tests/SessionTest.java
index f38618c..4cddc89 100644
--- a/tests/src/com/android/server/telecom/tests/SessionTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionTest.java
@@ -20,7 +20,8 @@
import android.telecom.Log;
import android.telecom.Logging.Session;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
@@ -268,6 +269,6 @@
}
private Session createTestSession(String name, String methodName) {
- return new Session(name, methodName, 0, false, null);
+ return new Session(name, methodName, 0, false, false ,null);
}
}
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
index dc7d1fd..169d580 100644
--- a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
@@ -24,7 +24,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Matchers.any;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
@@ -44,7 +44,8 @@
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.Uri;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.SystemStateHelper;
import com.android.server.telecom.SystemStateHelper.SystemStateListener;
diff --git a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
new file mode 100644
index 0000000..4d494f3
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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 com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.StatsManager;
+import android.os.HandlerThread;
+import android.util.StatsEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.AudioRouteStats;
+import com.android.server.telecom.metrics.CallStats;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class TelecomMetricsControllerTest extends TelecomTestCase {
+
+ @Mock
+ ApiStats mApiStats;
+ @Mock
+ AudioRouteStats mAudioRouteStats;
+ @Mock
+ CallStats mCallStats;
+ @Mock
+ ErrorStats mErrorStats;
+
+ HandlerThread mHandlerThread;
+
+ TelecomMetricsController mTelecomMetricsController;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mHandlerThread = new HandlerThread("TelecomMetricsControllerTest");
+ mHandlerThread.start();
+ mTelecomMetricsController = TelecomMetricsController.make(mContext, mHandlerThread);
+ assertThat(mTelecomMetricsController).isNotNull();
+ setUpStats();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ mTelecomMetricsController.destroy();
+ mHandlerThread.quitSafely();
+ super.tearDown();
+ }
+
+ @Test
+ public void testGetApiStatsReturnsSameInstance() {
+ ApiStats stats1 = mTelecomMetricsController.getApiStats();
+ ApiStats stats2 = mTelecomMetricsController.getApiStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testGetAudioRouteStatsReturnsSameInstance() {
+ AudioRouteStats stats1 = mTelecomMetricsController.getAudioRouteStats();
+ AudioRouteStats stats2 = mTelecomMetricsController.getAudioRouteStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testGetCallStatsReturnsSameInstance() {
+ CallStats stats1 = mTelecomMetricsController.getCallStats();
+ CallStats stats2 = mTelecomMetricsController.getCallStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testGetErrorStatsReturnsSameInstance() {
+ ErrorStats stats1 = mTelecomMetricsController.getErrorStats();
+ ErrorStats stats2 = mTelecomMetricsController.getErrorStats();
+ assertThat(stats1).isSameInstanceAs(stats2);
+ }
+
+ @Test
+ public void testOnPullAtomReturnsPullSkipIfAtomNotRegistered() {
+ mTelecomMetricsController.getStats().clear();
+
+ int result = mTelecomMetricsController.onPullAtom(TELECOM_API_STATS, null);
+ assertThat(result).isEqualTo(StatsManager.PULL_SKIP);
+ }
+
+ @Test
+ public void testRegisterAtom() {
+ StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+ ApiStats stats = mock(ApiStats.class);
+
+ mTelecomMetricsController.registerAtom(TELECOM_API_STATS, stats);
+
+ verify(statsManager, times(1)).setPullAtomCallback(eq(TELECOM_API_STATS), anyObject(),
+ anyObject(), eq(mTelecomMetricsController));
+ assertThat(mTelecomMetricsController.getStats().get(TELECOM_API_STATS))
+ .isSameInstanceAs(stats);
+ }
+
+ @Test
+ public void testDestroy() {
+ StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+ mTelecomMetricsController.destroy();
+
+ verify(statsManager, times(1)).clearPullAtomCallback(eq(CALL_AUDIO_ROUTE_STATS));
+ verify(statsManager, times(1)).clearPullAtomCallback(eq(CALL_STATS));
+ verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_API_STATS));
+ verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_ERROR_STATS));
+ assertThat(mTelecomMetricsController.getStats()).isEmpty();
+ }
+
+ @Test
+ public void testOnPullAtomIsPulled() {
+ final List<StatsEvent> data = new ArrayList<>();
+ final ArgumentCaptor<List<StatsEvent>> captor = ArgumentCaptor.forClass((Class) List.class);
+ doReturn(StatsManager.PULL_SUCCESS).when(mApiStats).pull(any());
+
+ int result = mTelecomMetricsController.onPullAtom(TELECOM_API_STATS, data);
+
+ verify(mApiStats).pull(captor.capture());
+ assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS);
+ assertThat(captor.getValue()).isEqualTo(data);
+ }
+
+ private void setUpStats() {
+ mTelecomMetricsController.getStats().put(CALL_AUDIO_ROUTE_STATS,
+ mAudioRouteStats);
+ mTelecomMetricsController.getStats().put(CALL_STATS, mCallStats);
+ mTelecomMetricsController.getStats().put(TELECOM_API_STATS, mApiStats);
+ mTelecomMetricsController.getStats().put(TELECOM_ERROR_STATS, mErrorStats);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
new file mode 100644
index 0000000..d3c7859
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
@@ -0,0 +1,1014 @@
+/*
+ * 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 com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_LE;
+import static com.android.server.telecom.AudioRoute.TYPE_EARPIECE;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_INCOMING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.StatsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Looper;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.util.StatsEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.AudioRouteStats;
+import com.android.server.telecom.metrics.CallStats;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+public class TelecomPulledAtomTest extends TelecomTestCase {
+ private static final long MIN_PULL_INTERVAL_MILLIS = 23L * 60 * 60 * 1000;
+ private static final long DEFAULT_TIMESTAMPS_MILLIS = 3000;
+ private static final int DELAY_FOR_PERSISTENT_MILLIS = 30000;
+ private static final int DELAY_TOLERANCE = 50;
+ private static final int TEST_TIMEOUT = (int) AudioRouteStats.THRESHOLD_REVERT_MS + 1000;
+ private static final String FILE_NAME_TEST_ATOM = "test_atom.pb";
+
+ private static final int VALUE_ATOM_COUNT = 1;
+
+ private static final int VALUE_UID = 10000 + 1;
+ private static final int VALUE_API_ID = 1;
+ private static final int VALUE_API_RESULT = 1;
+ private static final int VALUE_API_COUNT = 1;
+
+ private static final int VALUE_AUDIO_ROUTE_TYPE1 = 1;
+ private static final int VALUE_AUDIO_ROUTE_TYPE2 = 2;
+ private static final int VALUE_AUDIO_ROUTE_COUNT = 1;
+ private static final int VALUE_AUDIO_ROUTE_LATENCY = 300;
+
+ private static final int VALUE_CALL_DIRECTION = 1;
+ private static final int VALUE_CALL_ACCOUNT_TYPE = 1;
+ private static final int VALUE_CALL_COUNT = 1;
+ private static final int VALUE_CALL_DURATION = 3000;
+
+ private static final int VALUE_MODULE_ID = 1;
+ private static final int VALUE_ERROR_ID = 1;
+ private static final int VALUE_ERROR_COUNT = 1;
+
+ @Rule
+ public TemporaryFolder mTempFolder = new TemporaryFolder();
+ @Mock
+ FileOutputStream mFileOutputStream;
+ @Mock
+ PendingAudioRoute mMockPendingAudioRoute;
+ @Mock
+ AudioRoute mMockSourceRoute;
+ @Mock
+ AudioRoute mMockDestRoute;
+ private File mTempFile;
+ private Looper mLooper;
+ private Context mSpyContext;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mSpyContext = spy(mContext);
+ mLooper = Looper.getMainLooper();
+ mTempFile = mTempFolder.newFile(FILE_NAME_TEST_ATOM);
+ doReturn(mTempFile).when(mSpyContext).getFileStreamPath(anyString());
+ doReturn(mFileOutputStream).when(mSpyContext).openFileOutput(anyString(), anyInt());
+ doReturn(mMockSourceRoute).when(mMockPendingAudioRoute).getOrigRoute();
+ doReturn(mMockDestRoute).when(mMockPendingAudioRoute).getDestRoute();
+ doReturn(TYPE_EARPIECE).when(mMockSourceRoute).getType();
+ doReturn(TYPE_BLUETOOTH_LE).when(mMockDestRoute).getType();
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ mTempFile.delete();
+ super.tearDown();
+ }
+
+ @Test
+ public void testNewPulledAtomsFromFileInvalid() throws Exception {
+ mTempFile.delete();
+
+ ApiStats apiStats = new ApiStats(mSpyContext, mLooper);
+
+ assertNotNull(apiStats.mPulledAtoms);
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 0);
+
+ AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper);
+
+ assertNotNull(audioRouteStats.mPulledAtoms);
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 0);
+
+ CallStats callStats = new CallStats(mSpyContext, mLooper);
+
+ assertNotNull(callStats.mPulledAtoms);
+ assertEquals(callStats.mPulledAtoms.callStats.length, 0);
+
+ ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper);
+
+ assertNotNull(errorStats.mPulledAtoms);
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 0);
+ }
+
+ @Test
+ public void testNewPulledAtomsFromFileValid() throws Exception {
+ createTestFileForApiStats(DEFAULT_TIMESTAMPS_MILLIS);
+ ApiStats apiStats = new ApiStats(mSpyContext, mLooper);
+
+ verifyTestDataForApiStats(apiStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+ createTestFileForAudioRouteStats(DEFAULT_TIMESTAMPS_MILLIS);
+ AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper);
+
+ verifyTestDataForAudioRouteStats(audioRouteStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+ createTestFileForCallStats(DEFAULT_TIMESTAMPS_MILLIS);
+ CallStats callStats = new CallStats(mSpyContext, mLooper);
+
+ verifyTestDataForCallStats(callStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+ createTestFileForErrorStats(DEFAULT_TIMESTAMPS_MILLIS);
+ ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper);
+
+ verifyTestDataForErrorStats(errorStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+ }
+
+ @Test
+ public void testPullApiStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = apiStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(apiStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullApiStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = apiStats.mPulledAtoms.telecomApiStats.length;
+
+ int result = apiStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(apiStats).onPull(eq(data));
+ assertEquals(data.size(), sizePulled);
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 0);
+ }
+
+ @Test
+ public void testPullAudioRouteStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = audioRouteStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(audioRouteStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullAudioRouteStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = audioRouteStats.mPulledAtoms.callAudioRouteStats.length;
+
+ int result = audioRouteStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(audioRouteStats).onPull(eq(data));
+ assertEquals(data.size(), sizePulled);
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 0);
+ }
+
+ @Test
+ public void testPullCallStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = callStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(callStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullCallStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = callStats.mPulledAtoms.callStats.length;
+
+ int result = callStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(callStats).onPull(eq(data));
+ assertEquals(data.size(), sizePulled);
+ assertEquals(callStats.mPulledAtoms.callStats.length, 0);
+ }
+
+ @Test
+ public void testPullErrorStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+ createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+
+ int result = errorStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SKIP, result);
+ verify(errorStats, never()).onPull(any());
+ assertEquals(data.size(), 0);
+ }
+
+ @Test
+ public void testPullErrorStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+ createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+ final List<StatsEvent> data = new ArrayList<>();
+ int sizePulled = errorStats.mPulledAtoms.telecomErrorStats.length;
+
+ int result = errorStats.pull(data);
+
+ assertEquals(StatsManager.PULL_SUCCESS, result);
+ verify(errorStats).onPull(eq(data));
+ assertEquals(data.size(), sizePulled);
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 0);
+ }
+
+ @Test
+ public void testApiStatsLogCount() throws Exception {
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
+
+ for (int i = 0; i < 10; i++) {
+ apiStats.log(event);
+ waitForHandlerAction(apiStats, TEST_TIMEOUT);
+
+ verify(apiStats, times(i + 1)).onAggregate();
+ verify(apiStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 1);
+ verifyMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats[0], VALUE_API_ID,
+ VALUE_UID, VALUE_API_RESULT, i + 1);
+ }
+ }
+
+ @Test
+ public void testApiStatsLogEvent() throws Exception {
+ final int[] apis = {
+ ApiStats.API_UNSPECIFIC,
+ ApiStats.API_ACCEPTHANDOVER,
+ ApiStats.API_ACCEPTRINGINGCALL,
+ ApiStats.API_ACCEPTRINGINGCALLWITHVIDEOSTATE,
+ ApiStats.API_ADDCALL,
+ ApiStats.API_ADDNEWINCOMINGCALL,
+ ApiStats.API_ADDNEWINCOMINGCONFERENCE,
+ ApiStats.API_ADDNEWUNKNOWNCALL,
+ ApiStats.API_CANCELMISSEDCALLSNOTIFICATION,
+ ApiStats.API_CLEARACCOUNTS,
+ ApiStats.API_CREATELAUNCHEMERGENCYDIALERINTENT,
+ ApiStats.API_CREATEMANAGEBLOCKEDNUMBERSINTENT,
+ ApiStats.API_DUMP,
+ ApiStats.API_DUMPCALLANALYTICS,
+ ApiStats.API_ENABLEPHONEACCOUNT,
+ ApiStats.API_ENDCALL,
+ ApiStats.API_GETADNURIFORPHONEACCOUNT,
+ ApiStats.API_GETALLPHONEACCOUNTHANDLES,
+ ApiStats.API_GETALLPHONEACCOUNTS,
+ ApiStats.API_GETALLPHONEACCOUNTSCOUNT,
+ ApiStats.API_GETCALLCAPABLEPHONEACCOUNTS,
+ ApiStats.API_GETCALLSTATE,
+ ApiStats.API_GETCALLSTATEUSINGPACKAGE,
+ ApiStats.API_GETCURRENTTTYMODE,
+ ApiStats.API_GETDEFAULTDIALERPACKAGE,
+ ApiStats.API_GETDEFAULTDIALERPACKAGEFORUSER,
+ ApiStats.API_GETDEFAULTOUTGOINGPHONEACCOUNT,
+ ApiStats.API_GETDEFAULTPHONEAPP,
+ ApiStats.API_GETLINE1NUMBER,
+ ApiStats.API_GETOWNSELFMANAGEDPHONEACCOUNTS,
+ ApiStats.API_GETPHONEACCOUNT,
+ ApiStats.API_GETPHONEACCOUNTSFORPACKAGE,
+ ApiStats.API_GETPHONEACCOUNTSSUPPORTINGSCHEME,
+ ApiStats.API_GETREGISTEREDPHONEACCOUNTS,
+ ApiStats.API_GETSELFMANAGEDPHONEACCOUNTS,
+ ApiStats.API_GETSIMCALLMANAGER,
+ ApiStats.API_GETSIMCALLMANAGERFORUSER,
+ ApiStats.API_GETSYSTEMDIALERPACKAGE,
+ ApiStats.API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+ ApiStats.API_GETVOICEMAILNUMBER,
+ ApiStats.API_HANDLEPINMMI,
+ ApiStats.API_HANDLEPINMMIFORPHONEACCOUNT,
+ ApiStats.API_HASMANAGEONGOINGCALLSPERMISSION,
+ ApiStats.API_ISINCALL,
+ ApiStats.API_ISINCOMINGCALLPERMITTED,
+ ApiStats.API_ISINEMERGENCYCALL,
+ ApiStats.API_ISINMANAGEDCALL,
+ ApiStats.API_ISINSELFMANAGEDCALL,
+ ApiStats.API_ISOUTGOINGCALLPERMITTED,
+ ApiStats.API_ISRINGING,
+ ApiStats.API_ISTTYSUPPORTED,
+ ApiStats.API_ISVOICEMAILNUMBER,
+ ApiStats.API_PLACECALL,
+ ApiStats.API_REGISTERPHONEACCOUNT,
+ ApiStats.API_SETDEFAULTDIALER,
+ ApiStats.API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+ ApiStats.API_SHOWINCALLSCREEN,
+ ApiStats.API_SILENCERINGER,
+ ApiStats.API_STARTCONFERENCE,
+ ApiStats.API_UNREGISTERPHONEACCOUNT,
+ };
+ final int[] results = {ApiStats.RESULT_UNKNOWN, ApiStats.RESULT_NORMAL,
+ ApiStats.RESULT_EXCEPTION, ApiStats.RESULT_PERMISSION};
+ ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+ Random rand = new Random();
+ Map<ApiStats.ApiEvent, Integer> eventMap = new HashMap<>();
+
+ for (int i = 0; i < 10; i++) {
+ int api = apis[rand.nextInt(apis.length)];
+ int uid = rand.nextInt(65535);
+ int result = results[rand.nextInt(results.length)];
+ ApiStats.ApiEvent event = new ApiStats.ApiEvent(api, uid, result);
+ eventMap.put(event, eventMap.getOrDefault(event, 0) + 1);
+
+ apiStats.log(event);
+ waitForHandlerAction(apiStats, TEST_TIMEOUT);
+
+ verify(apiStats, times(i + 1)).onAggregate();
+ verify(apiStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, eventMap.size());
+ assertTrue(hasMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats,
+ api, uid, result, eventMap.get(event)));
+ }
+ }
+
+ @Test
+ public void testAudioRouteStatsLog() throws Exception {
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.log(VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ waitForHandlerAction(audioRouteStats, TEST_TIMEOUT);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false, 1,
+ VALUE_AUDIO_ROUTE_LATENCY);
+
+ audioRouteStats.log(VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ waitForHandlerAction(audioRouteStats, TEST_TIMEOUT);
+
+ verify(audioRouteStats, times(2)).onAggregate();
+ verify(audioRouteStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false, 2,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnEnterThenExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute, true);
+ waitForHandlerAction(audioRouteStats, 100);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the stats should be saved when the revert threshold is expired
+ waitForHandlerActionDelayed(
+ audioRouteStats, TEST_TIMEOUT, AudioRouteStats.THRESHOLD_REVERT_MS);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, false, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRevertToSourceInThreshold() throws Exception {
+ int delay = 100;
+ int latency = 500;
+ int duration = 1000;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute, true);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the event should be saved as revert when routing back to the source before
+ // the revert threshold is expired
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, duration);
+
+ // Reverse the audio types
+ doReturn(TYPE_BLUETOOTH_LE).when(mMockSourceRoute).getType();
+ doReturn(TYPE_EARPIECE).when(mMockDestRoute).getType();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, true, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRevertToSourceBeyondThreshold() throws Exception {
+ int delay = 100;
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute, true);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the event should not be saved as revert when routing back to the source
+ // after the revert threshold is expired
+ waitForHandlerActionDelayed(
+ audioRouteStats, TEST_TIMEOUT, AudioRouteStats.THRESHOLD_REVERT_MS);
+
+ // Reverse the audio types
+ doReturn(TYPE_BLUETOOTH_LE).when(mMockSourceRoute).getType();
+ doReturn(TYPE_EARPIECE).when(mMockDestRoute).getType();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, false, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRouteToAnotherDestInThreshold() throws Exception {
+ int delay = 100;
+ int latency = 500;
+ int duration = 1000;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute, true);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ // Verify that the stats should not be saved before the revert threshold is expired
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+
+ // Verify that the event should not be saved as revert when routing to a type different
+ // as the source before the revert threshold is expired
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, duration);
+
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerAction(audioRouteStats, delay);
+
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 1);
+ verifyMessageForAudioRouteStats(audioRouteStats.mPulledAtoms.callAudioRouteStats[0],
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE,
+ CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE, true, false, 1,
+ latency);
+ }
+
+ @Test
+ public void testAudioRouteStatsOnMultipleEnterWithoutExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ doReturn(mMockDestRoute).when(mMockPendingAudioRoute).getOrigRoute();
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Verify that the stats should not be saved without exit
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+ }
+
+ @Test
+ public void testAudioRouteStatsOnMultipleEnterWithExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+ audioRouteStats.onRouteExit(mMockPendingAudioRoute, true);
+ waitForHandlerAction(audioRouteStats, 100);
+
+ doReturn(mMockDestRoute).when(mMockPendingAudioRoute).getOrigRoute();
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Verify that the stats should be saved after exit
+ verify(audioRouteStats, times(1)).onAggregate();
+ verify(audioRouteStats, times(1)).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+ }
+
+ @Test
+ public void testAudioRouteStatsOnRouteToSameDestWithExit() throws Exception {
+ int latency = 500;
+ AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+ doReturn(mMockSourceRoute).when(mMockPendingAudioRoute).getDestRoute();
+
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Enter again to trigger the log
+ AudioRoute dest2 = mock(AudioRoute.class);
+ doReturn(TYPE_SPEAKER).when(dest2).getType();
+ doReturn(dest2).when(mMockPendingAudioRoute).getDestRoute();
+ audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
+ waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
+
+ // Verify that the stats should not be saved without exit
+ verify(audioRouteStats, never()).onAggregate();
+ verify(audioRouteStats, never()).save(anyInt());
+ assertTrue(audioRouteStats.hasMessages(AudioRouteStats.EVENT_REVERT_THRESHOLD_EXPIRED));
+ }
+
+ @Test
+ public void testCallStatsLog() throws Exception {
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+
+ callStats.log(VALUE_CALL_DIRECTION, false, false, true, VALUE_CALL_ACCOUNT_TYPE,
+ VALUE_UID, VALUE_CALL_DURATION);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(1)).onAggregate();
+ verify(callStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(callStats.mPulledAtoms.callStats.length, 1);
+ verifyMessageForCallStats(callStats.mPulledAtoms.callStats[0], VALUE_CALL_DIRECTION,
+ false, false, true, VALUE_CALL_ACCOUNT_TYPE, VALUE_UID, 1, VALUE_CALL_DURATION);
+
+ callStats.log(VALUE_CALL_DIRECTION, false, false, true, VALUE_CALL_ACCOUNT_TYPE,
+ VALUE_UID, VALUE_CALL_DURATION);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(2)).onAggregate();
+ verify(callStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(callStats.mPulledAtoms.callStats.length, 1);
+ verifyMessageForCallStats(callStats.mPulledAtoms.callStats[0], VALUE_CALL_DIRECTION,
+ false, false, true, VALUE_CALL_ACCOUNT_TYPE, VALUE_UID, 2, VALUE_CALL_DURATION);
+ }
+
+ @Test
+ public void testCallStatsOnStartThenEnd() throws Exception {
+ int duration = 1000;
+ int fakeUid = 10010;
+ PhoneAccount account = mock(PhoneAccount.class);
+ Call.CallingPackageIdentity callingPackage = new Call.CallingPackageIdentity();
+ PackageManager pm = mock(PackageManager.class);
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.uid = fakeUid;
+ doReturn(ai).when(pm).getApplicationInfo(any(), anyInt());
+ doReturn(pm).when(mSpyContext).getPackageManager();
+ Context fakeContext = spy(mContext);
+ doReturn("").when(fakeContext).getPackageName();
+ ComponentName cn = new ComponentName(fakeContext, this.getClass());
+ PhoneAccountHandle handle = mock(PhoneAccountHandle.class);
+ doReturn(cn).when(handle).getComponentName();
+ Call call = mock(Call.class);
+ doReturn(true).when(call).isIncoming();
+ doReturn(account).when(call).getPhoneAccountFromHandle();
+ doReturn((long) duration).when(call).getAgeMillis();
+ doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_CALL_PROVIDER));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
+ doReturn(callingPackage).when(call).getCallingPackageIdentity();
+ doReturn(handle).when(call).getTargetPhoneAccount();
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+
+ callStats.onCallStart(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ callStats.onCallEnd(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(1)).log(eq(CALL_STATS__CALL_DIRECTION__DIR_INCOMING),
+ eq(false), eq(false), eq(false), eq(CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM),
+ eq(fakeUid), eq(duration));
+ }
+
+ @Test
+ public void testCallStatsOnMultipleAudioDevices() throws Exception {
+ int duration = 1000;
+ int fakeUid = 10010;
+ PhoneAccount account = mock(PhoneAccount.class);
+ Call.CallingPackageIdentity callingPackage = new Call.CallingPackageIdentity();
+ PackageManager pm = mock(PackageManager.class);
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.uid = fakeUid;
+ doReturn(ai).when(pm).getApplicationInfo(any(), anyInt());
+ doReturn(pm).when(mSpyContext).getPackageManager();
+ Context fakeContext = spy(mContext);
+ doReturn("").when(fakeContext).getPackageName();
+ ComponentName cn = new ComponentName(fakeContext, this.getClass());
+ PhoneAccountHandle handle = mock(PhoneAccountHandle.class);
+ doReturn(cn).when(handle).getComponentName();
+ Call call = mock(Call.class);
+ doReturn(true).when(call).isIncoming();
+ doReturn(account).when(call).getPhoneAccountFromHandle();
+ doReturn((long) duration).when(call).getAgeMillis();
+ doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_CALL_PROVIDER));
+ doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
+ doReturn(callingPackage).when(call).getCallingPackageIdentity();
+ doReturn(handle).when(call).getTargetPhoneAccount();
+ CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+
+ callStats.onCallStart(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ callStats.onAudioDevicesChange(true);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ callStats.onCallEnd(call);
+ waitForHandlerAction(callStats, TEST_TIMEOUT);
+
+ verify(callStats, times(1)).log(eq(CALL_STATS__CALL_DIRECTION__DIR_INCOMING),
+ eq(false), eq(false), eq(true), eq(CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM),
+ eq(fakeUid), eq(duration));
+ }
+
+ @Test
+ public void testErrorStatsLogCount() throws Exception {
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+ for (int i = 0; i < 10; i++) {
+ errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
+ waitForHandlerAction(errorStats, TEST_TIMEOUT);
+
+ verify(errorStats, times(i + 1)).onAggregate();
+ verify(errorStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 1);
+ verifyMessageForErrorStats(errorStats.mPulledAtoms.telecomErrorStats[0],
+ VALUE_MODULE_ID,
+ VALUE_ERROR_ID, i + 1);
+ }
+ }
+
+ @Test
+ public void testErrorStatsLogEvent() throws Exception {
+ ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+ int[] modules = {
+ ErrorStats.SUB_UNKNOWN,
+ ErrorStats.SUB_CALL_AUDIO,
+ ErrorStats.SUB_CALL_LOGS,
+ ErrorStats.SUB_CALL_MANAGER,
+ ErrorStats.SUB_CONNECTION_SERVICE,
+ ErrorStats.SUB_EMERGENCY_CALL,
+ ErrorStats.SUB_IN_CALL_SERVICE,
+ ErrorStats.SUB_MISC,
+ ErrorStats.SUB_PHONE_ACCOUNT,
+ ErrorStats.SUB_SYSTEM_SERVICE,
+ ErrorStats.SUB_TELEPHONY,
+ ErrorStats.SUB_UI,
+ ErrorStats.SUB_VOIP_CALL,
+ };
+ int[] errors = {
+ ErrorStats.ERROR_UNKNOWN,
+ ErrorStats.ERROR_EXTERNAL_EXCEPTION,
+ ErrorStats.ERROR_INTERNAL_EXCEPTION,
+ ErrorStats.ERROR_AUDIO_ROUTE_RETRY_REJECTED,
+ ErrorStats.ERROR_BT_GET_SERVICE_FAILURE,
+ ErrorStats.ERROR_BT_REGISTER_CALLBACK_FAILURE,
+ ErrorStats.ERROR_AUDIO_ROUTE_UNAVAILABLE,
+ ErrorStats.ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE,
+ ErrorStats.ERROR_NOTIFY_CALL_STREAM_START_FAILURE,
+ ErrorStats.ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE,
+ ErrorStats.ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE,
+ ErrorStats.ERROR_RTT_STREAM_CLOSE_FAILURE,
+ ErrorStats.ERROR_RTT_STREAM_CREATE_FAILURE,
+ ErrorStats.ERROR_SET_MUTED_FAILURE,
+ ErrorStats.ERROR_VIDEO_PROVIDER_SET_FAILURE,
+ ErrorStats.ERROR_WIRED_HEADSET_NOT_AVAILABLE,
+ ErrorStats.ERROR_LOG_CALL_FAILURE,
+ ErrorStats.ERROR_RETRIEVING_ACCOUNT_EMERGENCY,
+ ErrorStats.ERROR_RETRIEVING_ACCOUNT,
+ ErrorStats.ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT,
+ ErrorStats.ERROR_DEFAULT_MO_ACCOUNT_MISMATCH,
+ ErrorStats.ERROR_ESTABLISHING_CONNECTION,
+ ErrorStats.ERROR_REMOVING_CALL,
+ ErrorStats.ERROR_STUCK_CONNECTING_EMERGENCY,
+ ErrorStats.ERROR_STUCK_CONNECTING,
+ };
+ Random rand = new Random();
+ Map<Long, Integer> eventMap = new HashMap<>();
+
+ for (int i = 0; i < 10; i++) {
+ int module = modules[rand.nextInt(modules.length)];
+ int error = errors[rand.nextInt(errors.length)];
+ long key = (long) module << 32 | error;
+ eventMap.put(key, eventMap.getOrDefault(key, 0) + 1);
+
+ errorStats.log(module, error);
+ waitForHandlerAction(errorStats, DELAY_TOLERANCE);
+
+ verify(errorStats, times(i + 1)).onAggregate();
+ verify(errorStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+ assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, eventMap.size());
+ assertTrue(hasMessageForErrorStats(
+ errorStats.mPulledAtoms.telecomErrorStats, module, error, eventMap.get(key)));
+ }
+ }
+
+ private void createTestFileForApiStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.telecomApiStats =
+ new PulledAtomsClass.TelecomApiStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.telecomApiStats[i] = new PulledAtomsClass.TelecomApiStats();
+ atom.telecomApiStats[i].setApiName(VALUE_API_ID + i);
+ atom.telecomApiStats[i].setUid(VALUE_UID);
+ atom.telecomApiStats[i].setApiResult(VALUE_API_RESULT);
+ atom.telecomApiStats[i].setCount(VALUE_API_COUNT);
+ }
+ atom.setTelecomApiStatsPullTimestampMillis(timestamps);
+
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForApiStats(final PulledAtomsClass.PulledAtoms atom,
+ long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getTelecomApiStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.telecomApiStats);
+ assertEquals(atom.telecomApiStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.telecomApiStats[i]);
+ verifyMessageForApiStats(atom.telecomApiStats[i], VALUE_API_ID + i, VALUE_UID,
+ VALUE_API_RESULT, VALUE_API_COUNT);
+ }
+ }
+
+ private void verifyMessageForApiStats(final PulledAtomsClass.TelecomApiStats msg, int apiId,
+ int uid, int result, int count) {
+ assertEquals(msg.getApiName(), apiId);
+ assertEquals(msg.getUid(), uid);
+ assertEquals(msg.getApiResult(), result);
+ assertEquals(msg.getCount(), count);
+ }
+
+ private boolean hasMessageForApiStats(final PulledAtomsClass.TelecomApiStats[] msgs, int apiId,
+ int uid, int result, int count) {
+ for (PulledAtomsClass.TelecomApiStats msg : msgs) {
+ if (msg.getApiName() == apiId && msg.getUid() == uid && msg.getApiResult() == result
+ && msg.getCount() == count) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void createTestFileForAudioRouteStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.callAudioRouteStats =
+ new PulledAtomsClass.CallAudioRouteStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.callAudioRouteStats[i] = new PulledAtomsClass.CallAudioRouteStats();
+ atom.callAudioRouteStats[i].setCallAudioRouteSource(VALUE_AUDIO_ROUTE_TYPE1);
+ atom.callAudioRouteStats[i].setCallAudioRouteDest(VALUE_AUDIO_ROUTE_TYPE2);
+ atom.callAudioRouteStats[i].setSuccess(true);
+ atom.callAudioRouteStats[i].setRevert(false);
+ atom.callAudioRouteStats[i].setCount(VALUE_AUDIO_ROUTE_COUNT);
+ atom.callAudioRouteStats[i].setAverageLatencyMs(VALUE_AUDIO_ROUTE_LATENCY);
+ }
+ atom.setCallAudioRouteStatsPullTimestampMillis(timestamps);
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForAudioRouteStats(final PulledAtomsClass.PulledAtoms atom,
+ long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getCallAudioRouteStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.callAudioRouteStats);
+ assertEquals(atom.callAudioRouteStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.callAudioRouteStats[i]);
+ verifyMessageForAudioRouteStats(atom.callAudioRouteStats[i], VALUE_AUDIO_ROUTE_TYPE1,
+ VALUE_AUDIO_ROUTE_TYPE2, true, false, VALUE_AUDIO_ROUTE_COUNT,
+ VALUE_AUDIO_ROUTE_LATENCY);
+ }
+ }
+
+ private void verifyMessageForAudioRouteStats(
+ final PulledAtomsClass.CallAudioRouteStats msg, int source, int dest, boolean success,
+ boolean revert, int count, int latency) {
+ assertEquals(msg.getCallAudioRouteSource(), source);
+ assertEquals(msg.getCallAudioRouteDest(), dest);
+ assertEquals(msg.getSuccess(), success);
+ assertEquals(msg.getRevert(), revert);
+ assertEquals(msg.getCount(), count);
+ assertTrue(Math.abs(latency - msg.getAverageLatencyMs()) < DELAY_TOLERANCE);
+ }
+
+ private void createTestFileForCallStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.callStats =
+ new PulledAtomsClass.CallStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.callStats[i] = new PulledAtomsClass.CallStats();
+ atom.callStats[i].setCallDirection(VALUE_CALL_DIRECTION);
+ atom.callStats[i].setExternalCall(false);
+ atom.callStats[i].setEmergencyCall(false);
+ atom.callStats[i].setMultipleAudioAvailable(false);
+ atom.callStats[i].setAccountType(VALUE_CALL_ACCOUNT_TYPE);
+ atom.callStats[i].setUid(VALUE_UID);
+ atom.callStats[i].setCount(VALUE_CALL_COUNT);
+ atom.callStats[i].setAverageDurationMs(VALUE_CALL_DURATION);
+ }
+ atom.setCallStatsPullTimestampMillis(timestamps);
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForCallStats(final PulledAtomsClass.PulledAtoms atom,
+ long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getCallStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.callStats);
+ assertEquals(atom.callStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.callStats[i]);
+ verifyMessageForCallStats(atom.callStats[i], VALUE_CALL_DIRECTION, false, false,
+ false, VALUE_CALL_ACCOUNT_TYPE, VALUE_UID, VALUE_CALL_COUNT,
+ VALUE_CALL_DURATION);
+ }
+ }
+
+ private void verifyMessageForCallStats(final PulledAtomsClass.CallStats msg,
+ int direction, boolean external, boolean emergency, boolean multipleAudio,
+ int accountType, int uid, int count, int duration) {
+ assertEquals(msg.getCallDirection(), direction);
+ assertEquals(msg.getExternalCall(), external);
+ assertEquals(msg.getEmergencyCall(), emergency);
+ assertEquals(msg.getMultipleAudioAvailable(), multipleAudio);
+ assertEquals(msg.getAccountType(), accountType);
+ assertEquals(msg.getUid(), uid);
+ assertEquals(msg.getCount(), count);
+ assertEquals(msg.getAverageDurationMs(), duration);
+ }
+
+ private void createTestFileForErrorStats(long timestamps) throws IOException {
+ PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+ atom.telecomErrorStats =
+ new PulledAtomsClass.TelecomErrorStats[VALUE_ATOM_COUNT];
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ atom.telecomErrorStats[i] = new PulledAtomsClass.TelecomErrorStats();
+ atom.telecomErrorStats[i].setSubmodule(VALUE_MODULE_ID);
+ atom.telecomErrorStats[i].setError(VALUE_ERROR_ID);
+ atom.telecomErrorStats[i].setCount(VALUE_ERROR_COUNT);
+ }
+ atom.setTelecomErrorStatsPullTimestampMillis(timestamps);
+ FileOutputStream stream = new FileOutputStream(mTempFile);
+ stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+ stream.close();
+ }
+
+ private void verifyTestDataForErrorStats(
+ final PulledAtomsClass.PulledAtoms atom, long timestamps) {
+ assertNotNull(atom);
+ assertEquals(atom.getTelecomErrorStatsPullTimestampMillis(), timestamps);
+ assertNotNull(atom.telecomErrorStats);
+ assertEquals(atom.telecomErrorStats.length, VALUE_ATOM_COUNT);
+ for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+ assertNotNull(atom.telecomErrorStats[i]);
+ verifyMessageForErrorStats(atom.telecomErrorStats[i], VALUE_MODULE_ID, VALUE_ERROR_ID
+ , VALUE_ERROR_COUNT);
+ }
+ }
+
+ private void verifyMessageForErrorStats(final PulledAtomsClass.TelecomErrorStats msg,
+ int moduleId, int errorId, int count) {
+ assertEquals(msg.getSubmodule(), moduleId);
+ assertEquals(msg.getError(), errorId);
+ assertEquals(msg.getCount(), count);
+ }
+
+ private boolean hasMessageForErrorStats(final PulledAtomsClass.TelecomErrorStats[] msgs,
+ int moduleId, int errorId, int count) {
+ for (PulledAtomsClass.TelecomErrorStats msg : msgs) {
+ if (msg.getSubmodule() == moduleId && msg.getError() == errorId
+ && msg.getCount() == count) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 8bc1f2a..6b0555c 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -23,6 +23,36 @@
import static android.Manifest.permission.READ_PHONE_NUMBERS;
import static android.Manifest.permission.READ_PHONE_STATE;
import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
+import static android.Manifest.permission.READ_SMS;
+import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+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.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.Manifest;
import android.app.ActivityManager;
@@ -48,7 +78,8 @@
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
@@ -58,14 +89,17 @@
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.InCallController;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.TelecomServiceImpl;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
-import com.android.server.telecom.voip.IncomingCallTransaction;
-import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
import org.junit.After;
import org.junit.Before;
@@ -76,40 +110,15 @@
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
+import java.lang.reflect.Method;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;
-import static android.Manifest.permission.READ_SMS;
-import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
-import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-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.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.isA;
-import static org.mockito.Mockito.when;
-
@RunWith(JUnit4.class)
public class TelecomServiceImplTest extends TelecomTestCase {
@@ -122,7 +131,7 @@
public static class CallIntentProcessAdapterFake implements CallIntentProcessor.Adapter {
@Override
public void processOutgoingCallIntent(Context context, CallsManager callsManager,
- Intent intent, String callingPackage) {
+ Intent intent, String callingPackage, FeatureFlags flags) {
}
@@ -192,9 +201,15 @@
@Mock private ICallEventCallback mICallEventCallback;
@Mock private TransactionManager mTransactionManager;
@Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
+ @Mock private FeatureFlags mFeatureFlags;
+ @Mock private com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+
+ @Mock private InCallController mInCallController;
+ @Mock private TelecomMetricsController mMockTelecomMetricsController;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
+ private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
private static final String DEFAULT_DIALER_PACKAGE = "com.google.android.dialer";
private static final UserHandle USER_HANDLE_16 = new UserHandle(16);
private static final UserHandle USER_HANDLE_17 = new UserHandle(17);
@@ -219,6 +234,7 @@
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
+ when(mFakeCallsManager.getInCallController()).thenReturn(mInCallController);
doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class),
anyString());
when(mContext.checkCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS))
@@ -242,7 +258,11 @@
mDefaultDialerCache,
mSubscriptionManagerAdapter,
mSettingsSecureAdapter,
- mLock);
+ mFeatureFlags,
+ mTelephonyFeatureFlags,
+ mLock,
+ mMockTelecomMetricsController,
+ SYSTEM_UI_PACKAGE);
telecomServiceImpl.setTransactionManager(mTransactionManager);
telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
mTSIBinder = telecomServiceImpl.getBinder();
@@ -260,6 +280,8 @@
mPackageManager = mContext.getPackageManager();
when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
+ when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true);
+ when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
}
@Override
@@ -525,10 +547,64 @@
assertEquals(fullPHList,
mTSIBinder.getCallCapablePhoneAccounts(
- true, DEFAULT_DIALER_PACKAGE, null).getList());
+ true, DEFAULT_DIALER_PACKAGE, null, false).getList());
assertEquals(smallPHList,
mTSIBinder.getCallCapablePhoneAccounts(
- false, DEFAULT_DIALER_PACKAGE, null).getList());
+ false, DEFAULT_DIALER_PACKAGE, null, false).getList());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetCallCapablePhoneAccountsAcrossProfiles() throws RemoteException {
+ List<PhoneAccountHandle> fullPHList = List.of(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+ List<PhoneAccountHandle> smallPHList = List.of(SIP_PA_HANDLE_17);
+
+ // Returns all accounts when getCallCapablePhoneAccounts is called with across user.
+ doReturn(fullPHList).when(mFakePhoneAccountRegistrar).getCallCapablePhoneAccounts(
+ nullable(String.class), anyBoolean(), nullable(UserHandle.class), eq(true));
+ // Returns one account when getCallCapablePhoneAccounts is called without across user.
+ doReturn(smallPHList).when(mFakePhoneAccountRegistrar).getCallCapablePhoneAccounts(
+ nullable(String.class), anyBoolean(), nullable(UserHandle.class), eq(false));
+ // With across user permission
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+ eq(Manifest.permission.INTERACT_ACROSS_USERS));
+
+ assertEquals(fullPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(
+ true, DEFAULT_DIALER_PACKAGE, null, false).getList());
+
+ // Without across user permission
+ doReturn(PackageManager.PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+ eq(Manifest.permission.INTERACT_ACROSS_USERS));
+
+ assertEquals(smallPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(
+ true, DEFAULT_DIALER_PACKAGE, null, false).getList());
+
+ // Enabled the feature flag of the work profile split mode
+ when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(true);
+
+ // With across user permission
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+ eq(Manifest.permission.INTERACT_ACROSS_PROFILES));
+
+ assertEquals(fullPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(
+ true, DEFAULT_DIALER_PACKAGE, null, true).getList());
+ assertEquals(smallPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(
+ true, DEFAULT_DIALER_PACKAGE, null, false).getList());
+
+ // Without across user permission
+ doReturn(PackageManager.PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+ eq(Manifest.permission.INTERACT_ACROSS_PROFILES));
+
+ assertEquals(smallPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(
+ true, DEFAULT_DIALER_PACKAGE, null, true).getList());
+ assertEquals(smallPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(
+ true, DEFAULT_DIALER_PACKAGE, null, false).getList());
}
@SmallTest
@@ -540,7 +616,7 @@
argThat(new AnyStringIn(enforcedPermissions)), anyString());
assertThrows(SecurityException.class,
- () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null));
+ () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null, false));
}
@SmallTest
@@ -778,6 +854,62 @@
@SmallTest
@Test
+ public void testRegisterPhoneAccountSimultaneousCallingVerification() throws RemoteException {
+ doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+ String packageNameToUse = "com.android.officialpackage";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "test", Binder.getCallingUserHandle());
+ PhoneAccountHandle phAllowedRestriction = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "test2", Binder.getCallingUserHandle());
+
+ PhoneAccount phoneAccountEmptyRestriction = makePhoneAccount(phHandle)
+ .setSimultaneousCallingRestriction(Collections.emptySet())
+ .build();
+ try {
+ mTSIBinder.registerPhoneAccount(phoneAccountEmptyRestriction, CALLING_PACKAGE);
+ verify(mFakePhoneAccountRegistrar).registerPhoneAccount(phoneAccountEmptyRestriction);
+ } catch (SecurityException e) {
+ fail("registerPhoneAccount must not throw a SecurityException if there is a "
+ + " restriction registered with the same package name.");
+ }
+
+ Set<PhoneAccountHandle> restriction = new HashSet<>(3);
+ restriction.add(phAllowedRestriction);
+ PhoneAccount phoneAccount = makePhoneAccount(phHandle)
+ .setSimultaneousCallingRestriction(restriction)
+ .build();
+
+ try {
+ mTSIBinder.registerPhoneAccount(phoneAccount, CALLING_PACKAGE);
+ verify(mFakePhoneAccountRegistrar).registerPhoneAccount(phoneAccount);
+ } catch (SecurityException e) {
+ fail("registerPhoneAccount must not throw a SecurityException if there is a "
+ + " restriction registered with the same package name.");
+ }
+
+ String anotherPackageName = "com.android.anotherpackage";
+ PhoneAccountHandle phDisallowedRestriction = new PhoneAccountHandle(new ComponentName(
+ anotherPackageName, "cs"), "test", Binder.getCallingUserHandle());
+ restriction.add(phDisallowedRestriction);
+ phoneAccount = makePhoneAccount(phHandle)
+ .setSimultaneousCallingRestriction(restriction)
+ .build();
+
+ try {
+ mTSIBinder.registerPhoneAccount(phoneAccount, CALLING_PACKAGE);
+ // there should not be another call to registerPhoneAccount
+ verify(mFakePhoneAccountRegistrar, times(1)).registerPhoneAccount(phoneAccount);
+ fail("registerPhoneAccount must throw a SecurityException if there is a "
+ + " restriction registered with a different package name.");
+ } catch (SecurityException e) {
+ //expected
+ }
+ }
+
+ @SmallTest
+ @Test
public void testRegisterPhoneAccountWithoutPermissionAnomalyReported() throws RemoteException {
PhoneAccountHandle handle = new PhoneAccountHandle(
new ComponentName("package", "cs"), "test", Binder.getCallingUserHandle());
@@ -1040,6 +1172,7 @@
verify(mFakePhoneAccountRegistrar).getPhoneAccount(
TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
+ verify(mInCallController, never()).bindToServices(any());
addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
TEL_PA_HANDLE_16, false);
@@ -1047,6 +1180,81 @@
@SmallTest
@Test
+ public void testAddNewIncomingFlagDisabledNoEarlyBinding() throws Exception {
+ when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(false);
+ PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+ phoneAccount.setIsEnabled(true);
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+ eq(TEL_PA_HANDLE_16));
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+ Bundle extras = createSampleExtras();
+
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+ verify(mInCallController, never()).bindToServices(eq(null));
+ }
+
+ @SmallTest
+ @Test
+ public void testAddNewIncomingCallEarlyBindingForNoCallFilterCalls() throws Exception {
+ PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+ phoneAccount.setIsEnabled(true);
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+ eq(TEL_PA_HANDLE_16));
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+ Bundle extras = createSampleExtras();
+
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+ verify(mInCallController).bindToServices(eq(null));
+ }
+
+ @SmallTest
+ @Test
+ public void testAddNewIncomingCallEarlyBindingNotEnableForNonWatchDevices() throws Exception {
+ PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+ phoneAccount.setIsEnabled(true);
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+ eq(TEL_PA_HANDLE_16));
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+ Bundle extras = createSampleExtras();
+
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+ verify(mInCallController, never()).bindToServices(eq(null));
+ }
+
+ @SmallTest
+ @Test
+ public void testAddNewIncomingCallEarlyBindingNotEnableForPhoneAccountHasCallFilters()
+ throws Exception {
+ PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_16).build();
+ phoneAccount.setIsEnabled(true);
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+ eq(TEL_PA_HANDLE_16));
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+ Bundle extras = createSampleExtras();
+
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+ verify(mInCallController, never()).bindToServices(eq(null));
+ }
+
+
+ @SmallTest
+ @Test
public void testAddNewIncomingCallFailure() throws Exception {
try {
mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null, CALLING_PACKAGE);
@@ -1703,6 +1911,28 @@
verify(mContext, never()).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
}
+ /**
+ * FeatureFlags is autogenerated code, so there could be a situation where something changes
+ * outside of Telecom control that breaks reflection. This test attempts to ensure that changes
+ * to auto-generated FeatureFlags code that breaks reflection are caught early.
+ */
+ @SmallTest
+ @Test
+ public void testFlagConfigReflectionWorks() {
+ try {
+ Method[] methods = FeatureFlags.class.getMethods();
+ for (Method m : methods) {
+ // test getting the name and invoking the flag code
+ String name = m.getName();
+ Object val = m.invoke(mFeatureFlags);
+ assertNotNull(name);
+ assertNotNull(val);
+ }
+ } catch (Exception e) {
+ fail("Reflection failed for FeatureFlags with error: " + e);
+ }
+ }
+
@SmallTest
@Test
public void testIsVoicemailNumber() throws Exception {
@@ -1867,6 +2097,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";
@@ -2062,7 +2309,8 @@
}
/**
- * Ensure self-managed calls cannot be ended using {@link TelecomManager#endCall()}.
+ * Ensure self-managed calls cannot be ended using {@link TelecomManager#endCall()} when the
+ * caller of this method is not considered privileged.
* @throws Exception
*/
@SmallTest
@@ -2079,7 +2327,8 @@
/**
* Ensure self-managed calls cannot be answered using {@link TelecomManager#acceptRingingCall()}
- * or {@link TelecomManager#acceptRingingCall(int)}.
+ * or {@link TelecomManager#acceptRingingCall(int)} when the caller of these methods is not
+ * considered privileged.
* @throws Exception
*/
@SmallTest
@@ -2094,6 +2343,53 @@
verify(mFakeCallsManager, never()).answerCall(eq(call), anyInt());
}
+ /**
+ * Ensure self-managed calls can be answered using {@link TelecomManager#acceptRingingCall()}
+ * or {@link TelecomManager#acceptRingingCall(int)} if the caller of these methods is
+ * privileged.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testCanAnswerSelfManagedCallIfPrivileged() throws Exception {
+ when(mFeatureFlags.allowSystemAppsResolveVoipCalls()).thenReturn(true);
+ // Configure the test so that the caller of acceptRingingCall is considered privileged:
+ when(mPackageManager.getPackageUid(SYSTEM_UI_PACKAGE, 0))
+ .thenReturn(Binder.getCallingUid());
+
+ // Ensure that the call is successfully accepted:
+ Call call = mock(Call.class);
+ when(call.isSelfManaged()).thenReturn(true);
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ when(mFakeCallsManager.getFirstCallWithState(any()))
+ .thenReturn(call);
+ mTSIBinder.acceptRingingCall(TEST_PACKAGE);
+ verify(mFakeCallsManager).answerCall(eq(call), anyInt());
+ }
+
+ /**
+ * Ensure self-managed calls can be ended using {@link TelecomManager#endCall()} when the
+ * caller of these methods is privileged.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testCanEndSelfManagedCallIfPrivileged() throws Exception {
+ when(mFeatureFlags.allowSystemAppsResolveVoipCalls()).thenReturn(true);
+ // Configure the test so that the caller of endCall is considered privileged:
+ when(mPackageManager.getPackageUid(SYSTEM_UI_PACKAGE, 0))
+ .thenReturn(Binder.getCallingUid());
+ // Set up the call:
+ Call call = mock(Call.class);
+ when(call.isSelfManaged()).thenReturn(true);
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ when(mFakeCallsManager.getFirstCallWithState(any()))
+ .thenReturn(call);
+ // Ensure that the call is successfully ended:
+ assertTrue(mTSIBinder.endCall(TEST_PACKAGE));
+ verify(mFakeCallsManager).disconnectCall(eq(call));
+ }
+
@SmallTest
@Test
public void testGetAdnUriForPhoneAccount() throws Exception {
@@ -2144,6 +2440,12 @@
return new PhoneAccount.Builder(paHandle, "testLabel");
}
+ private PhoneAccount.Builder makeSkipCallFilteringPhoneAccount(PhoneAccountHandle paHandle) {
+ Bundle extras = new Bundle();
+ extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
+ return new PhoneAccount.Builder(paHandle, "testLabel").setExtras(extras);
+ }
+
private Bundle createSampleExtras() {
Bundle extras = new Bundle();
extras.putString("test_key", "test_value");
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index fb35125..1e65011 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -22,11 +22,11 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -38,6 +38,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
@@ -69,6 +70,7 @@
import com.android.internal.telecom.IInCallAdapter;
import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
import com.android.server.telecom.CallAudioManager;
import com.android.server.telecom.CallAudioModeStateMachine;
import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -98,6 +100,7 @@
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.google.common.base.Predicate;
@@ -119,7 +122,7 @@
/**
* Implements mocks and functionality required to implement telecom system tests.
*/
-public class TelecomSystemTest extends TelecomTestCase {
+public class TelecomSystemTest extends TelecomTestCase{
private static final String CALLING_PACKAGE = TelecomSystemTest.class.getPackageName();
static final int TEST_POLL_INTERVAL = 10; // milliseconds
@@ -167,7 +170,7 @@
}
@Override
- public void showMissedCallNotification(CallInfo call) {
+ public void showMissedCallNotification(CallInfo call, @Nullable Uri uri) {
missedCallsNotified.add(call);
}
@@ -214,7 +217,14 @@
@Mock Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
@Mock
BlockedNumbersAdapter mBlockedNumbersAdapter;
+ @Mock
+ CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ @Mock
+ FeatureFlags mFeatureFlags;
+ @Mock
+ com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
+ private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
final ComponentName mInCallServiceComponentNameX =
new ComponentName(
"incall-service-package-X",
@@ -320,6 +330,20 @@
PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
.build();
+ final PhoneAccount mPhoneAccountMultiUser =
+ PhoneAccount.builder(
+ new PhoneAccountHandle(
+ mConnectionServiceComponentNameA,
+ "id MU", UserHandle.of(12)),
+ "Phone account service MU")
+ .addSupportedUriScheme("tel")
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_CALL_PROVIDER |
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+ PhoneAccount.CAPABILITY_VIDEO_CALLING |
+ PhoneAccount.CAPABILITY_MULTI_USER)
+ .build();
+
ConnectionServiceFixture mConnectionServiceFixtureA;
ConnectionServiceFixture mConnectionServiceFixtureB;
Timeouts.Adapter mTimeoutsAdapter;
@@ -493,9 +517,12 @@
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(),
- (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter)
+ (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter,
+ mFeatureFlag)
-> mMissedCallNotifier,
mCallerInfoAsyncQueryFactoryFixture.getTestDouble(),
headsetMediaButtonFactory,
@@ -518,7 +545,9 @@
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
int earpieceControl,
- Executor asyncTaskExecutor) {
+ Executor asyncTaskExecutor,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
return new CallAudioRouteStateMachine(context,
callsManager,
bluetoothManager,
@@ -528,15 +557,20 @@
// Force enable an earpiece for the end-to-end tests
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
mHandlerThread.getLooper(),
- Runnable::run /* async tasks as now sync for testing! */);
+ Runnable::run /* async tasks as now sync for testing! */,
+ communicationDeviceTracker,
+ featureFlags);
}
},
new CallAudioModeStateMachine.Factory() {
@Override
public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
- AudioManager am) {
+ AudioManager am, FeatureFlags featureFlags,
+ CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker
+ ) {
return new CallAudioModeStateMachine(systemStateHelper, am,
- mHandlerThread.getLooper());
+ mHandlerThread.getLooper(), featureFlags,
+ callAudioCommunicationDeviceTracker);
}
},
mClockProxy,
@@ -547,10 +581,13 @@
ContactsAsyncHelper.ContentResolverAdapter adapter) {
return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
}
- }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
+ }, mDeviceIdleControllerAdapter, SYSTEM_UI_PACKAGE,
+ mAccessibilityManagerAdapter,
Runnable::run,
Runnable::run,
- mBlockedNumbersAdapter);
+ mBlockedNumbersAdapter,
+ mFeatureFlags,
+ mTelephonyFlags);
mComponentContextFixture.setTelecomManager(new TelecomManager(
mComponentContextFixture.getTestDouble(),
@@ -584,6 +621,7 @@
mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountB0);
mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE0);
mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE1);
+ mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountMultiUser);
mTelecomSystem.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(
mPhoneAccountA0.getAccountHandle(), Process.myUserHandle());
@@ -768,7 +806,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/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index 5353bc6..5b5c3ed 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -22,6 +22,9 @@
import androidx.test.InstrumentationRegistry;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -33,6 +36,8 @@
public abstract class TelecomTestCase {
protected static final String TESTING_TAG = "Telecom-TEST";
protected Context mContext;
+ @Mock
+ FeatureFlags mFeatureFlags;
MockitoHelper mMockitoHelper = new MockitoHelper();
ComponentContextFixture mComponentContextFixture;
@@ -42,11 +47,12 @@
Log.setIsExtendedLoggingEnabled(true);
Log.setUnitTestingEnabled(true);
mMockitoHelper.setUp(InstrumentationRegistry.getContext(), getClass());
- mComponentContextFixture = new ComponentContextFixture();
- mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
- Log.setSessionContext(mComponentContextFixture.getTestDouble().getApplicationContext());
- Log.getSessionManager().mCleanStaleSessions = null;
MockitoAnnotations.initMocks(this);
+
+ mComponentContextFixture = new ComponentContextFixture(mFeatureFlags);
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ Log.setSessionManager(mComponentContextFixture.getTestDouble().getApplicationContext(),
+ null);
}
public void tearDown() throws Exception {
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 3fc87a9..78c2210 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,21 +16,32 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+ .TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+ .VideoProfileStateToTransactionalVideoState;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.isA;
+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.isA;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
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;
@@ -38,30 +49,41 @@
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;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceWrapper;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
+import com.android.server.telecom.callsequencing.voip.HoldCallTransaction;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.MaybeHoldCallForNewCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.RequestNewActiveCallTransaction;
import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.HoldCallTransaction;
-import com.android.server.telecom.voip.IncomingCallTransaction;
-import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
public class TransactionTests extends TelecomTestCase {
@@ -90,6 +112,7 @@
super.setUp();
MockitoAnnotations.initMocks(this);
Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
}
@Override
@@ -191,14 +214,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));
}
@@ -209,7 +232,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");
@@ -236,7 +260,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()))
@@ -250,6 +275,154 @@
isA(Boolean.class));
}
+ /**
+ * 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() {
+ when(mFeatureFlags.transactionalCsVerifier()).thenReturn(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);
+ t.timeout();
+
+ // THEN
+ verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
+ verify(listener).onTransactionTimeout(anyString());
+ verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
+ }
+
+ /**
+ * This test verifies that when an application transitions a call to the requested state,
+ * Telecom does not disconnect the call and transaction completes successfully.
+ */
+ @SmallTest
+ @Test
+ public void testCallStateIsSuccessfullyChanged()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ when(mFeatureFlags.transactionalCsVerifier()).thenReturn(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);
+ 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(CallTransactionResult.RESULT_SUCCEED,
+ t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
+ verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
+ }
+
private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) {
when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
@@ -267,7 +440,8 @@
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
Call callSpy = Mockito.spy(call);
@@ -280,4 +454,12 @@
return callSpy;
}
+
+ private void setupHoldableCall(){
+ when(mMockCall1.getState()).thenReturn(CallState.ACTIVE);
+ when(mMockCall1.getConnectionServiceWrapper()).thenReturn(
+ mock(ConnectionServiceWrapper.class));
+ doNothing().when(mMockCall1).addCallStateListener(any());
+ doReturn(true).when(mMockCall1).removeCallStateListener(any());
+ }
}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fa5f2a2..fea6135 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -39,10 +39,10 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.TransactionalServiceRepository;
import com.android.server.telecom.TransactionalServiceWrapper;
-import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.HoldCallTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
+import com.android.server.telecom.callsequencing.voip.HoldCallTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
import org.junit.After;
import org.junit.Before;
@@ -83,9 +83,8 @@
Mockito.when(mCallsManager.getLock()).thenReturn(mLock);
Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
- mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
-
- mTransactionalServiceWrapper.setTransactionManager(mTransactionManager);
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
+ false /*call sequencing*/);
}
@Override
@@ -98,7 +97,8 @@
public void testTransactionalServiceWrapperStartState() throws Exception {
TransactionalServiceWrapper service =
new TransactionalServiceWrapper(mCallEventCallback,
- mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
+ false /*call sequencing*/);
assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
assertEquals(1, service.getNumberOfTrackedCalls());
@@ -108,7 +108,8 @@
public void testTransactionalServiceWrapperCallCount() throws Exception {
TransactionalServiceWrapper service =
new TransactionalServiceWrapper(mCallEventCallback,
- mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
+ false /*call sequencing*/);
assertEquals(1, service.getNumberOfTrackedCalls());
service.trackCall(mMockCall2);
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
index 84beedc..0ce70af 100644
--- a/tests/src/com/android/server/telecom/tests/VideoCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
@@ -16,6 +16,26 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+
+import android.os.Process;
+import android.os.RemoteException;
+import android.telecom.CallAudioState;
+import android.telecom.DisconnectCause;
+import android.telecom.VideoProfile;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
+
+import com.android.server.telecom.CallAudioModeStateMachine;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -23,26 +43,8 @@
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
-import android.os.Process;
-import android.os.RemoteException;
-import android.telecom.CallAudioState;
-import android.telecom.DisconnectCause;
-import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
-
-import com.android.server.telecom.CallAudioModeStateMachine;
-import com.android.server.telecom.CallAudioRouteStateMachine;
-
import java.util.List;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-
/**
* System tests for video-specific behavior in telecom.
* TODO: Add unit tests which ensure that auto-speakerphone does not occur when using a wired
@@ -258,13 +260,13 @@
*/
private void verifyAudioRoute(int expectedRoute) throws Exception {
// Capture all onCallAudioStateChanged callbacks to InCall.
- CallAudioRouteStateMachine carsm = mTelecomSystem.getCallsManager()
- .getCallAudioManager().getCallAudioRouteStateMachine();
+ CallAudioRouteAdapter cara = mTelecomSystem.getCallsManager()
+ .getCallAudioManager().getCallAudioRouteAdapter();
CallAudioModeStateMachine camsm = mTelecomSystem.getCallsManager()
.getCallAudioManager().getCallAudioModeStateMachine();
waitForHandlerAction(camsm.getHandler(), TEST_TIMEOUT);
final boolean[] success = {true};
- carsm.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> {
+ cara.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> {
ArgumentCaptor<CallAudioState> callAudioStateArgumentCaptor = ArgumentCaptor.forClass(
CallAudioState.class);
try {
@@ -277,7 +279,7 @@
assertEquals(expectedRoute, changes.get(changes.size() - 1).getRoute());
success[0] = true;
});
- waitForHandlerAction(carsm.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(cara.getAdapterHandler(), TEST_TIMEOUT);
assertTrue(success[0]);
}
}
diff --git a/tests/src/com/android/server/telecom/tests/VideoProfileTest.java b/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
index 5ee0414..b2a1c81 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
@@ -21,7 +21,8 @@
import static org.junit.Assert.assertTrue;
import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
index 2b6c260..060e3ae 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
@@ -26,7 +26,8 @@
import android.os.IBinder;
import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.internal.telecom.IVideoProvider;
import com.android.server.telecom.Analytics;
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index 597924d..56fbf72 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -16,6 +16,41 @@
package com.android.server.telecom.tests;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+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.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Build;
+import android.os.UserHandle;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoCallImpl;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.view.Surface;
+
+import androidx.test.filters.MediumTest;
+
+import com.android.server.telecom.CallsManager;
+
+import com.google.common.base.Predicate;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -26,48 +61,10 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.graphics.SurfaceTexture;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.telecom.Call;
-import android.telecom.Connection;
-import android.telecom.Connection.VideoProvider;
-import android.telecom.DisconnectCause;
-import android.telecom.InCallService;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoCallImpl;
-import android.telecom.VideoProfile;
-import android.telecom.VideoProfile.CameraCapabilities;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.view.Surface;
-
-import com.google.common.base.Predicate;
-
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyLong;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-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.timeout;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import com.android.server.telecom.CallsManager;
-
/**
* Performs tests of the {@link VideoProvider} and {@link VideoCall} APIs. Ensures that requests
* sent from an InCallService are routed through Telecom to a VideoProvider, and that callbacks are
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index c66b0f7..bf68f8c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -16,6 +16,11 @@
package com.android.server.telecom.tests;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -38,12 +43,13 @@
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.TelecomSystem;
-import com.android.server.telecom.voip.VoipCallMonitor;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
import org.junit.Before;
import org.junit.Test;
@@ -86,6 +92,31 @@
.thenReturn(true);
}
+ /**
+ * This test ensures VoipCallMonitor is passing the correct foregroundServiceTypes when starting
+ * foreground service delegation on behalf of a client.
+ */
+ @SmallTest
+ @Test
+ public void testVerifyForegroundServiceTypesBeingPassedToActivityManager() {
+ Call call = createTestCall("testCall", mHandle1User1);
+ ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor =
+ ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+
+ mMonitor.onCallAdded(call);
+
+ verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(
+ optionsCaptor.capture(), any(ServiceConnection.class));
+
+ assertEquals( FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+ FOREGROUND_SERVICE_TYPE_MICROPHONE |
+ FOREGROUND_SERVICE_TYPE_CAMERA |
+ FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+ optionsCaptor.getValue().mForegroundServiceTypes);
+
+ mMonitor.onCallRemoved(call);
+ }
+
@SmallTest
@Test
public void testStartMonitorForOneCall() {
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index 0a7e27d..c479aac 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -21,16 +21,15 @@
import android.os.OutcomeReceiver;
import android.telecom.CallException;
-import android.util.Log;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.TelecomSystem;
-import com.android.server.telecom.voip.ParallelTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.voip.ParallelTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
import org.junit.After;
import org.junit.Before;
@@ -52,14 +51,16 @@
private TransactionManager mTransactionManager;
private static final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
- private class TestVoipCallTransaction extends VoipCallTransaction {
+ private class TestVoipCallTransaction extends CallTransaction {
public static final int SUCCESS = 0;
public static final int FAILED = 1;
public static final int TIMEOUT = 2;
+ public static final int EXCEPTION = 3;
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);
@@ -69,28 +70,37 @@
}
@Override
- public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
- CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
+ public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+ if (mType == EXCEPTION) {
+ mLog.append(mName).append(" exception;\n");
+ throw new IllegalStateException("TEST EXCEPTION");
+ }
+ CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
mHandler.postDelayed(() -> {
if (mType == SUCCESS) {
mLog.append(mName).append(" success;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
null));
} else if (mType == FAILED) {
mLog.append(mName).append(" failed;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
null));
} else {
mLog.append(mName).append(" timeout;\n");
resultFuture.complete(
- new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
"timeout"));
}
}, mSleepTime);
return resultFuture;
}
+
+ @Override
+ public void finishTransaction() {
+ isFinished = true;
+ }
}
@Override
@@ -104,7 +114,6 @@
@Override
@After
public void tearDown() throws Exception {
- Log.i("Grace", mLog.toString());
mTransactionManager.clear();
super.tearDown();
}
@@ -113,46 +122,47 @@
@Test
public void testSerialTransactionSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
- List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ List<CallTransaction> subTransactions = new ArrayList<>();
+ 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);
subTransactions.add(t3);
- CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+ CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
resultFuture::complete;
String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
mTransactionManager.addTransaction(new SerialTransaction(subTransactions, mLock),
outcomeReceiver);
- assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+ assertEquals(CallTransactionResult.RESULT_SUCCEED,
resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
assertEquals(expectedLog, mLog.toString());
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@Test
public void testSerialTransactionFailed()
throws ExecutionException, InterruptedException, TimeoutException {
- List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ List<CallTransaction> subTransactions = new ArrayList<>();
+ 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);
subTransactions.add(t3);
CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
- new OutcomeReceiver<VoipCallTransactionResult, CallException>() {
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
+ new OutcomeReceiver<CallTransactionResult, CallException>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
+ public void onResult(CallTransactionResult result) {
}
@@ -166,54 +176,56 @@
exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
String expectedLog = "t1 success;\nt2 failed;\n";
assertEquals(expectedLog, mLog.toString());
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@Test
public void testParallelTransactionSuccess()
throws ExecutionException, InterruptedException, TimeoutException {
- List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ List<CallTransaction> subTransactions = new ArrayList<>();
+ 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);
subTransactions.add(t3);
- CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+ CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
resultFuture::complete;
mTransactionManager.addTransaction(new ParallelTransaction(subTransactions, mLock),
outcomeReceiver);
- assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+ assertEquals(CallTransactionResult.RESULT_SUCCEED,
resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
String log = mLog.toString();
assertTrue(log.contains("t1 success;\n"));
assertTrue(log.contains("t2 success;\n"));
assertTrue(log.contains("t3 success;\n"));
+ verifyTransactionsFinished(t1, t2, t3);
}
@SmallTest
@Test
public void testParallelTransactionFailed()
throws ExecutionException, InterruptedException, TimeoutException {
- List<VoipCallTransaction> subTransactions = new ArrayList<>();
- VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ List<CallTransaction> subTransactions = new ArrayList<>();
+ 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);
subTransactions.add(t3);
CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
+ public void onResult(CallTransactionResult result) {
}
@@ -226,19 +238,20 @@
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 =
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
new OutcomeReceiver<>() {
@Override
- public void onResult(VoipCallTransactionResult result) {
+ public void onResult(CallTransactionResult result) {
}
@@ -246,8 +259,133 @@
public void onError(CallException e) {
exceptionFuture.complete(e.getMessage());
}
- }; mTransactionManager.addTransaction(t, outcomeReceiver);
+ };
+ 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 {
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction.EXCEPTION);
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeExceptionReceiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ }
+
+ @Override
+ public void onError(CallException e) {
+ exceptionFuture.complete(e.getMessage());
+ }
+ };
+ mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+ // Transaction will timeout because the Exception caused the transaction to stop processing.
+ exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
+ assertTrue(mLog.toString().contains("t1 exception;\n"));
+ // Verify an exception in a processing a previous transaction does not stall the next one.
+ CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
+ resultFuture::complete;
+ mTransactionManager.addTransaction(t2, outcomeReceiver);
+ String expectedLog = "t1 exception;\nt2 success;\n";
+ assertEquals(CallTransactionResult.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<CallTransactionResult, CallException> outcomeExceptionReceiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult 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 {
+ TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeExceptionReceiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ throw new IllegalStateException("RESULT EXCEPTION");
+ }
+
+ @Override
+ public void onError(CallException e) {
+ }
+ };
+ mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeException2Receiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(CallTransactionResult result) {
+ }
+
+ @Override
+ public void onError(CallException e) {
+ throw new IllegalStateException("RESULT EXCEPTION");
+ }
+ };
+ mTransactionManager.addTransaction(t2, outcomeException2Receiver);
+ // Verify an exception in a previous transaction result does not stall the next one.
+ CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+ OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
+ resultFuture::complete;
+ mTransactionManager.addTransaction(t3, outcomeReceiver);
+ String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
+ assertEquals(CallTransactionResult.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);
+ }
}
}