[automerger skipped] Unbind CS if connection is not created within 15 seconds. am: 69a816bb5a -s ours am: daa2316298 -s ours am: fa374bfe47 -s ours am: 30c5f5d75d -s ours am: 96e254913f -s ours
am skip reason: skipped by grantmenke
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/30258347
Change-Id: I34a81007a4c76b20453e6878a4e0a0c6f584b70a
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 501b438..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,
@@ -98,8 +102,8 @@
platform_apis: true,
certificate: "platform",
jacoco: {
- include_filter: ["com.android.server.telecom.*"],
- exclude_filter: ["com.android.server.telecom.tests.*"],
+ include_filter: ["com.android.server.telecom.**"],
+ exclude_filter: ["com.android.server.telecom.tests.**"],
},
test_suites: ["device-tests"],
defaults: ["SettingsLibDefaults"],
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 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 0adb30c..0619308 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -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>
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 031b25d..cfd153b 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/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 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 appli."</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 e53631b..22ac1fc 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/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-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 df2c70c..4aeceef 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -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 48c7957..e395ef1 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -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 70f9bfc..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>
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 3fbac1f..3b5e342 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -26,10 +26,15 @@
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;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
@@ -39,6 +44,9 @@
*/
@VisibleForTesting
public class AsyncRingtonePlayer {
+ // Maximum amount of time we will delay playing a ringtone while waiting for audio routing to
+ // be ready.
+ private static final int PLAY_DELAY_TIMEOUT_MS = 1000;
// Message codes used with the ringtone thread.
private static final int EVENT_PLAY = 1;
private static final int EVENT_STOP = 2;
@@ -49,6 +57,23 @@
/** The current ringtone. Only used by the ringtone thread. */
private Ringtone mRingtone;
+ /**
+ * Set to true if we are setting up to play or are currently playing. False if we are stopping
+ * or have stopped playing.
+ */
+ private boolean mIsPlaying = false;
+
+ /**
+ * Set to true if BT HFP is active and audio is connected.
+ */
+ private boolean mIsBtActive = false;
+
+ /**
+ * A list of pending ringing ready latches, which are used to delay the ringing command until
+ * audio paths are set and ringing is ready.
+ */
+ private final ArrayList<CountDownLatch> mPendingRingingLatches = new ArrayList<>();
+
public AsyncRingtonePlayer() {
// Empty
}
@@ -58,23 +83,84 @@
* 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) {
+ 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);
postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
}
/** Stops playing the ringtone. */
public void stop() {
Log.d(this, "Posting stop.");
+ mIsPlaying = false;
postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
+ // Clear any pending ringing latches so that we do not have to wait for its timeout to pass
+ // before calling stop.
+ clearPendingRingingLatches();
+ }
+
+ /**
+ * Called when the BT HFP profile active state changes.
+ * @param isBtActive A BT device is connected and audio is active.
+ */
+ public void updateBtActiveState(boolean isBtActive) {
+ Log.i(this, "updateBtActiveState: " + isBtActive);
+ synchronized (mPendingRingingLatches) {
+ mIsBtActive = isBtActive;
+ if (isBtActive) mPendingRingingLatches.forEach(CountDownLatch::countDown);
+ }
+ }
+
+ /**
+ * Prepares a new ringing ready latch and tracks it in a list. Once the ready latch has been
+ * used, {@link #removePendingRingingReadyLatch(CountDownLatch)} must be called on this latch.
+ * @param isHfpDeviceConnected true if there is a HFP device connected.
+ * @return the newly prepared CountDownLatch
+ */
+ private CountDownLatch prepareRingingReadyLatch(boolean isHfpDeviceConnected) {
+ CountDownLatch latch = new CountDownLatch(1);
+ synchronized (mPendingRingingLatches) {
+ // We only want to delay ringing if BT is connected but not active yet.
+ boolean isDelayRequired = isHfpDeviceConnected && !mIsBtActive;
+ Log.i(this, "prepareRingingReadyLatch:"
+ + " connected=" + isHfpDeviceConnected
+ + ", BT active=" + mIsBtActive
+ + ", isDelayRequired=" + isDelayRequired);
+ if (!isDelayRequired) latch.countDown();
+ mPendingRingingLatches.add(latch);
+ }
+ return latch;
+ }
+
+ /**
+ * Remove a ringing ready latch that has been used and is no longer pending.
+ * @param l The latch to remove.
+ */
+ private void removePendingRingingReadyLatch(CountDownLatch l) {
+ synchronized (mPendingRingingLatches) {
+ mPendingRingingLatches.remove(l);
+ }
+ }
+
+ /**
+ * Count down all pending ringing ready latches and then clear the list.
+ */
+ private void clearPendingRingingLatches() {
+ synchronized (mPendingRingingLatches) {
+ mPendingRingingLatches.forEach(CountDownLatch::countDown);
+ mPendingRingingLatches.clear();
+ }
}
/**
@@ -126,9 +212,12 @@
* 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();
Log.continueSession(session, "ARP.hP");
@@ -136,17 +225,34 @@
// Don't bother with any of this if there is an EVENT_STOP waiting, but give the
// consumer a chance to do anything no matter what.
if (mHandler.hasMessages(EVENT_STOP)) {
+ Log.i(this, "handlePlay: skipping play early due to pending STOP");
+ removePendingRingingReadyLatch(ringingReadyLatch);
ringtoneConsumer.accept(null, /* stopped= */ true);
return;
}
Ringtone ringtone = null;
+ Uri ringtoneUri = null;
boolean hasStopped = false;
try {
- ringtone = ringtoneSupplier.get();
- // Ringtone supply can be slow. Re-check for stop event.
+ try {
+ Log.i(this, "handlePlay: delay ring for ready signal...");
+ boolean reachedZero = ringingReadyLatch.await(PLAY_DELAY_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+ Log.i(this, "handlePlay: ringing ready, timeout=" + !reachedZero);
+ } catch (InterruptedException e) {
+ Log.w(this, "handlePlay: latch exception: " + e);
+ }
+ 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)) {
+ Log.i(this, "handlePlay: skipping play due to pending STOP");
hasStopped = true;
- ringtone.stop(); // proactively release the ringtone.
+ if (ringtone != null) ringtone.stop(); // proactively release the ringtone.
return;
}
// setRingtone even if null - it also stops any current ringtone to be consistent
@@ -157,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()) {
@@ -168,7 +273,8 @@
mRingtone.play();
Log.i(this, "Play ringtone, looping.");
} finally {
- ringtoneConsumer.accept(ringtone, hasStopped);
+ removePendingRingingReadyLatch(ringingReadyLatch);
+ ringtoneConsumer.accept(new Pair(ringtoneUri, ringtone), hasStopped);
}
} finally {
Log.cancelSubsession(session);
@@ -196,11 +302,15 @@
}
}
+ /**
+ * @return true if we are currently preparing or playing a ringtone, false if we are not.
+ */
public boolean isPlaying() {
- return mRingtone != null;
+ return mIsPlaying;
}
private void setRingtone(@Nullable Ringtone ringtone) {
+ Log.i(this, "setRingtone: ringtone null=" + (ringtone == null));
// Make sure that any previously created instance of Ringtone is stopped so the MediaPlayer
// can be released, before replacing mRingtone with a new instance. This is always created
// as a looping Ringtone, so if not stopped it will keep playing on the background.
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 cf52ce9..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
*/
@@ -406,6 +443,13 @@
private int mCallerDisplayNamePresentation;
/**
+ * The remote connection service which is attempted or already connecting this call. This is set
+ * to a non-null value only when a connection manager phone account is in use. When set, this
+ * will correspond to the target phone account of the {@link Call}.
+ */
+ private ConnectionServiceWrapper mRemoteConnectionService;
+
+ /**
* The connection service which is attempted or already connecting this call.
*/
private ConnectionServiceWrapper mConnectionService;
@@ -615,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.
@@ -755,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.
@@ -788,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);
}
@@ -812,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 &&
@@ -824,7 +971,6 @@
mLock = lock;
mRepository = repository;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
- setHandle(handle);
mParticipants = participants;
mPostDialDigits = handle != null
? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
@@ -832,6 +978,7 @@
setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
mCallDirection = callDirection;
setTargetPhoneAccount(targetPhoneAccountHandle);
+ setHandle(handle);
mIsConference = isConference;
mShouldAttachToExistingConnection = shouldAttachToExistingConnection
|| callDirection == CALL_DIRECTION_INCOMING;
@@ -844,6 +991,8 @@
mStartRingTime = 0;
mCallStateChangedAtomWriter.setExistingCallCount(callsManager.getCalls().size());
+ mIsModifyStatePermissionGranted =
+ isModifyPhoneStatePermissionGranted(getDelegatePhoneAccountHandle());
}
/**
@@ -863,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,
@@ -881,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;
@@ -1121,7 +1273,6 @@
return (!mIsTransactionalCall ? mConnectionService : mTransactionalService);
}
- @VisibleForTesting
public int getState() {
return mState;
}
@@ -1220,7 +1371,7 @@
message, null));
}
- mDisconnectFuture.complete(true);
+ mDiagnosticCompleteFuture.complete(true);
} else {
Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
getId());
@@ -1242,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) {
@@ -1329,6 +1486,12 @@
Log.addEvent(this, event, stringData);
}
+ if (mFlags.transactionalCsVerifier()) {
+ for (CallStateListener listener : mCallStateListeners) {
+ listener.onCallStateChanged(newState);
+ }
+ }
+
mCallStateChangedAtomWriter
.setDisconnectCause(getDisconnectCause())
.setSelfManaged(isSelfManaged())
@@ -1472,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;
@@ -1485,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);
}
@@ -1500,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) {
@@ -1734,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);
}
}
}
@@ -1803,7 +1982,6 @@
PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, false);
}
- @VisibleForTesting
public boolean isIncoming() {
return mCallDirection == CALL_DIRECTION_INCOMING;
}
@@ -1907,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() {
@@ -1989,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;
@@ -2068,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 ||
@@ -2127,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));
@@ -2314,22 +2537,40 @@
@VisibleForTesting
public void setConnectionService(ConnectionServiceWrapper service) {
+ Log.i(this, "setConnectionService: service=[%s]", service);
+ setConnectionService(service, null);
+ }
+
+ @VisibleForTesting
+ public void setConnectionService(
+ ConnectionServiceWrapper service,
+ ConnectionServiceWrapper remoteService
+ ) {
Preconditions.checkNotNull(service);
clearConnectionService();
service.incrementAssociatedCallCount();
+
+ if (mFlags.updatedRcsCallCountTracking() && remoteService != null) {
+ remoteService.incrementAssociatedCallCount();
+ mRemoteConnectionService = remoteService;
+ }
+
mConnectionService = service;
mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
mConnectionService.addCall(this);
+ processCachedCallbacks(service);
}
/**
* Perform an in-place replacement of the {@link ConnectionServiceWrapper} for this Call.
- * Removes the call from its former {@link ConnectionServiceWrapper}, ensuring that the
- * ConnectionService is NOT unbound if the call count hits zero.
- * This is used by the {@link ConnectionServiceWrapper} when handling {@link Connection} and
- * {@link Conference} additions via a ConnectionManager.
+ * Removes the call from its former {@link ConnectionServiceWrapper}, while still ensuring the
+ * former {@link ConnectionServiceWrapper} is tracked as the mRemoteConnectionService for this
+ * call so that the associatedCallCount of that {@link ConnectionServiceWrapper} is accurately
+ * tracked until it is supposed to be unbound.
+ * This method is used by the {@link ConnectionServiceWrapper} when handling {@link Connection}
+ * and {@link Conference} additions via a ConnectionManager.
* The original {@link android.telecom.ConnectionService} will directly add external calls and
* conferences to Telecom as well as the ConnectionManager, which will add to Telecom. In these
* cases since its first added to via the original CS, we want to change the CS responsible for
@@ -2342,9 +2583,18 @@
if (mConnectionService != null) {
ConnectionServiceWrapper serviceTemp = mConnectionService;
+
+ 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);
- serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
+
+ if (!mFlags.updatedRcsCallCountTracking()) {
+ serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
+ }
}
service.incrementAssociatedCallCount();
@@ -2358,6 +2608,8 @@
void clearConnectionService() {
if (mConnectionService != null) {
ConnectionServiceWrapper serviceTemp = mConnectionService;
+ ConnectionServiceWrapper remoteServiceTemp = mRemoteConnectionService;
+ mRemoteConnectionService = null;
mConnectionService = null;
serviceTemp.removeCall(this);
@@ -2368,6 +2620,10 @@
// necessary, but cleaning up mConnectionService prior to triggering an unbind is good
// to do.
decrementAssociatedCallCount(serviceTemp);
+
+ if (mFlags.updatedRcsCallCountTracking() && remoteServiceTemp != null) {
+ decrementAssociatedCallCount(remoteServiceTemp);
+ }
}
}
@@ -2386,7 +2642,7 @@
return;
}
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
- phoneAccountRegistrar, mContext);
+ phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
mCreateConnectionProcessor.process();
}
@@ -2899,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(),
@@ -2914,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
@@ -3040,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);
@@ -3048,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}.
*
@@ -3264,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;
}
@@ -3273,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);
}
}
@@ -3557,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;
}
@@ -3577,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;
}
@@ -3652,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;
@@ -3664,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);
@@ -3675,8 +3937,6 @@
l.onCallerInfoChanged(this);
}
}
-
- Trace.endSection();
}
public CallerInfo getCallerInfo() {
@@ -3690,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) {
@@ -3931,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:
@@ -3946,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);
}
@@ -4033,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;
}
@@ -4183,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;
}
@@ -4505,6 +4781,7 @@
}
public void setStartFailCause(CallFailureCause cause) {
+ Log.i(this, "setStartFailCause: cause = %s; callId = %s", cause, this.getId());
mCallStateChangedAtomWriter.setStartFailCause(cause);
}
@@ -4557,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) -> {
@@ -4575,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;
}
/**
@@ -4590,7 +4867,7 @@
* if this is handled immediately.
*/
public boolean isDisconnectHandledViaFuture() {
- return mDisconnectFuture != null;
+ return mDiagnosticCompleteFuture != null;
}
/**
@@ -4598,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 d96f953..8a87c22 100644
--- a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -25,16 +25,19 @@
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) {
- mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+ DockManager dockManager,
+ AsyncRingtonePlayer ringtonePlayer) {
+ mCallAudioAdapter = callAudioRouteAdapter;
mBluetoothRouteManager = bluetoothManager;
+ mRingtonePlayer = ringtonePlayer;
mBluetoothRouteManager.setListener(this);
wiredHeadsetManager.addListener(this);
@@ -57,37 +60,47 @@
@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() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mRingtonePlayer.updateBtActiveState(true);
+ mCallAudioAdapter.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ }
+
+ @Override
+ public void onBluetoothAudioConnecting() {
+ mRingtonePlayer.updateBtActiveState(false);
+ // Pretend like audio is connected when communicating w/ CARSM.
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
}
@Override
public void onBluetoothAudioDisconnected() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mRingtonePlayer.updateBtActiveState(false);
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
}
@Override
public void onUnexpectedBluetoothStateChange() {
- mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ mCallAudioAdapter.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
}
@@ -98,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 4a03726..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
@@ -1215,7 +1212,13 @@
// Expected, since we just transitioned here
return HANDLED;
case SPEAKER_OFF:
- sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
+ // Check if we already requested to connect to other devices and just waiting
+ // for their response. In some cases, this SPEAKER_OFF message may come in
+ // before the response, we can just ignore the message here to not re-evaluate
+ // the baseline route incorrectly
+ if (!mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()) {
+ sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
+ }
return HANDLED;
case SWITCH_FOCUS:
if (msg.arg1 == NO_FOCUS) {
@@ -1514,6 +1517,8 @@
private CallAudioState mLastKnownCallAudioState;
private CallAudioManager mCallAudioManager;
+ private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+ private FeatureFlags mFeatureFlags;
public CallAudioRouteStateMachine(
Context context,
@@ -1523,7 +1528,9 @@
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
int earpieceControl,
- Executor asyncTaskExecutor) {
+ Executor asyncTaskExecutor,
+ CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+ FeatureFlags featureFlags) {
super(NAME);
mContext = context;
mCallsManager = callsManager;
@@ -1534,6 +1541,8 @@
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
mAsyncTaskExecutor = asyncTaskExecutor;
+ mCommunicationDeviceTracker = communicationDeviceTracker;
+ mFeatureFlags = featureFlags;
createStates(earpieceControl);
}
@@ -1545,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;
@@ -1556,7 +1567,8 @@
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
mAsyncTaskExecutor = asyncTaskExecutor;
-
+ mCommunicationDeviceTracker = communicationDeviceTracker;
+ mFeatureFlags = featureFlags;
createStates(earpieceControl);
}
@@ -1659,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) {
@@ -1673,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.
@@ -1702,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;
@@ -1729,7 +1764,7 @@
}
public void dumpPendingMessages(IndentingPrintWriter pw) {
- getHandler().getLooper().dump(pw::println, "");
+ getAdapterHandler().getLooper().dump(pw::println, "");
}
public boolean isHfpDeviceAvailable() {
@@ -1741,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) {
@@ -1863,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,
@@ -1980,6 +2008,58 @@
return false;
}
+ private boolean isWatchActiveOrOnlyWatchesAvailable() {
+ if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+ Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: Flag is disabled.");
+ return false;
+ }
+
+ boolean containsWatchDevice = false;
+ boolean containsNonWatchDevice = false;
+ Collection<BluetoothDevice> connectedBtDevices =
+ mBluetoothRouteManager.getConnectedDevices();
+
+ for (BluetoothDevice connectedDevice: connectedBtDevices) {
+ if (mBluetoothRouteManager.isWatch(connectedDevice)) {
+ containsWatchDevice = true;
+ } else {
+ containsNonWatchDevice = true;
+ }
+ }
+
+ // Don't ignore switch if watch is already the active device.
+ boolean isActiveDeviceWatch = mBluetoothRouteManager.isWatch(
+ mBluetoothRouteManager.getBluetoothAudioConnectedDevice());
+ Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: contains watch: %s, contains "
+ + "non-wearable device: %s, is active device a watch: %s.",
+ containsWatchDevice, containsNonWatchDevice, isActiveDeviceWatch);
+ return containsWatchDevice && !containsNonWatchDevice && !isActiveDeviceWatch;
+ }
+
+ private boolean processLegacySpeakerCommunicationDevice(boolean on) {
+ AudioDeviceInfo speakerDevice = null;
+ for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
+ if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ speakerDevice = info;
+ break;
+ }
+ }
+ boolean speakerOn = false;
+ if (speakerDevice != null && on) {
+ boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
+ if (result) {
+ speakerOn = true;
+ }
+ } else {
+ AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
+ if (curDevice != null
+ && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ mAudioManager.clearCommunicationDevice();
+ }
+ }
+ return speakerOn;
+ }
+
private int calculateBaselineRouteMessage(boolean isExplicitUserRequest,
boolean includeBluetooth) {
boolean isSkipEarpiece = false;
@@ -1992,7 +2072,7 @@
}
if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0
&& !mHasUserExplicitlyLeftBluetooth
- && includeBluetooth) {
+ && includeBluetooth && !isWatchActiveOrOnlyWatchesAvailable()) {
return isExplicitUserRequest ? USER_SWITCH_BLUETOOTH : SWITCH_BLUETOOTH;
} else if ((mAvailableRoutes & ROUTE_EARPIECE) != 0 && !isSkipEarpiece) {
return isExplicitUserRequest ? USER_SWITCH_EARPIECE : SWITCH_EARPIECE;
@@ -2030,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;
@@ -2048,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 3005656..4484e23 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -16,20 +16,29 @@
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;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
import android.location.Country;
import android.location.CountryDetector;
import android.location.Location;
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;
+import android.os.UserManager;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.telecom.Connection;
@@ -42,13 +51,17 @@
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.telephony.SubscriptionManager;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.Flags;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
+import java.util.UUID;
import java.util.stream.Stream;
/**
@@ -68,16 +81,19 @@
*/
private static class AddCallArgs {
public AddCallArgs(Context context, CallLog.AddCallParams params,
- @Nullable LogCallCompletedListener logCallCompletedListener) {
+ @Nullable LogCallCompletedListener logCallCompletedListener,
+ @NonNull Call call) {
this.context = context;
this.params = params;
this.logCallCompletedListener = logCallCompletedListener;
+ this.call = call;
}
// Since the members are accessed directly, we don't use the
// mXxxx notation.
public final Context context;
public final CallLog.AddCallParams params;
+ public final Call call;
@Nullable
public final LogCallCompletedListener logCallCompletedListener;
}
@@ -88,29 +104,41 @@
// TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that
// a conference was merged successfully
private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY";
+ private static final UUID LOG_CALL_FAILED_ANOMALY_ID =
+ UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702");
+ private static final String LOG_CALL_FAILED_ANOMALY_DESC =
+ "Based on the current user, Telecom detected failure to record a call to the call log.";
private final Context mContext;
private final CarrierConfigManager mCarrierConfigManager;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final MissedCallNotifier mMissedCallNotifier;
+ private AnomalyReporterAdapter mAnomalyReporterAdapter;
private static final String ACTION_CALLS_TABLE_ADD_ENTRY =
- "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
+ "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
private static final String PERMISSION_PROCESS_CALLLOG_INFO =
- "android.permission.PROCESS_CALLLOG_INFO";
+ "android.permission.PROCESS_CALLLOG_INFO";
private static final String CALL_TYPE = "callType";
private static final String CALL_DURATION = "duration";
- private Object mLock;
+ private 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) {
+ MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter,
+ FeatureFlags featureFlags) {
mContext = context;
mCarrierConfigManager = (CarrierConfigManager) mContext
.getSystemService(Context.CARRIER_CONFIG_SERVICE);
mPhoneAccountRegistrar = phoneAccountRegistrar;
mMissedCallNotifier = missedCallNotifier;
- mLock = new Object();
+ mAnomalyReporterAdapter = anomalyReporterAdapter;
+ mCountryCodeExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
+ mFeatureFlags = featureFlags;
}
@Override
@@ -149,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
@@ -180,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)) {
@@ -200,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;
}
@@ -218,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;
}
}
@@ -240,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 {
@@ -263,7 +303,7 @@
* {@link android.provider.CallLog.Calls#BLOCKED_TYPE}.
*/
void logCall(Call call, int callLogType,
- @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
+ @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
CallLog.AddCallParams.AddCallParametersBuilder paramBuilder =
new CallLog.AddCallParams.AddCallParametersBuilder();
@@ -328,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 {
@@ -378,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);
+ logCallCompletedListener, call);
Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber)
+ ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres="
+ call.getHandlePresentation());
@@ -402,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);
@@ -516,8 +574,18 @@
AddCallArgs c = callList[i];
mListeners[i] = c.logCallCompletedListener;
try {
- // May block.
result[i] = Calls.addCall(c.context, c.params);
+ 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
+ // the goal here is to try and identify potential anomalous conditions with
+ // logging calls.
+ mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
+ LOG_CALL_FAILED_ANOMALY_DESC);
+ }
} catch (Exception e) {
// This is very rare but may happen in legitimate cases.
// E.g. If the phone is encrypted and thus write request fails, it may cause
@@ -526,8 +594,10 @@
//
// We don't want to crash the whole process just because of that, so just log
// it instead.
- Log.e(TAG, e, "Exception raised during adding CallLog entry.");
+ Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId());
result[i] = null;
+ mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
+ LOG_CALL_FAILED_ANOMALY_DESC);
}
}
return result;
@@ -567,7 +637,7 @@
return Locale.getDefault().getCountry();
}
- return country.getCountryIso();
+ return country.getCountryCode();
}
/**
@@ -578,28 +648,38 @@
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;
}
}
+
+ /** Consumer to receive the country code if it changes. */
+ private void countryCodeConsumer(Country newCountry) {
+ Log.startSession("CLM.cCC");
+ try {
+ Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
+ synchronized (mLock) {
+ mCurrentCountry = newCountry;
+ mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
+ mAnomalyReporterAdapter = anomalyReporterAdapter;
+ }
}
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
new file mode 100644
index 0000000..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 77570c3..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,9 +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.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;
@@ -140,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;
@@ -210,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);
@@ -222,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";
/**
@@ -286,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,
@@ -419,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;
@@ -463,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()));
}
@@ -495,6 +534,8 @@
private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
private final MmiUtils mMmiUtils = new MmiUtils();
+
+ private TelecomMetricsController mMetricsController;
/**
* Listener to PhoneAccountRegistrar events.
*/
@@ -527,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);
}
}
@@ -572,10 +614,17 @@
CallAnomalyWatchdog callAnomalyWatchdog,
Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
Executor asyncTaskExecutor,
+ Executor asyncCallAudioTaskExecutor,
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;
@@ -594,28 +643,43 @@
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,
- asyncTaskExecutor
- );
- 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);
+ mDockManager,
+ asyncRingtonePlayer);
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
InCallTonePlayer.MediaPlayerFactory mediaPlayerFactory =
(resourceId, attributes) ->
@@ -624,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,
@@ -639,41 +703,65 @@
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);
mTtyManager = new TtyManager(context, mWiredHeadsetManager);
mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
- mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier);
+ mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier,
+ mAnomalyReporter, featureFlags);
mConnectionServiceRepository =
- new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
+ 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);
@@ -691,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());
@@ -700,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<>();
@@ -744,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.
@@ -760,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
@@ -779,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()
@@ -801,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,
@@ -859,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
@@ -884,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;
@@ -930,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()
@@ -944,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)) {
@@ -954,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");
@@ -973,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) {
@@ -986,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);
}
}
}
@@ -1244,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);
}
}
@@ -1299,7 +1430,7 @@
return mCallAudioManager;
}
- InCallController getInCallController() {
+ public InCallController getInCallController() {
return mInCallController;
}
@@ -1307,7 +1438,7 @@
return mCallEndpointController;
}
- EmergencyCallHelper getEmergencyCallHelper() {
+ public EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
@@ -1379,8 +1510,11 @@
}
@VisibleForTesting
- public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
- mAnomalyReporter = mAnomalyReporterAdapter;
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
+ mAnomalyReporter = anomalyReporterAdapter;
+ if (mCallLogManager != null) {
+ mCallLogManager.setAnomalyReporterAdapter(anomalyReporterAdapter);
+ }
}
void processIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -1425,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
@@ -1440,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);
@@ -1452,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) {
@@ -1572,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);
@@ -1653,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);
@@ -1771,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);
@@ -1793,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
@@ -1884,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);
@@ -1919,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) {
@@ -1966,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
@@ -2027,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(
@@ -2047,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);
@@ -2060,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");
@@ -2187,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;
@@ -2303,6 +2541,15 @@
PhoneAccountHandle phoneAccountHandle = clientExtras.getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ PhoneAccount account =
+ mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
+ boolean isSelfManaged = account != null && account.isSelfManaged();
+ // Enforce outgoing call restriction for conference calls. This is handled via
+ // UserCallIntentProcessor for normal MO calls.
+ if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser, null,
+ isSelfManaged, CallsManager.class.getCanonicalName(), mFeatureFlags)) {
+ return;
+ }
CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle,
clientExtras, initiatingUser, null/* originalIntent */, callingPackage,
true/* isconference*/);
@@ -2345,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 -> {
@@ -2511,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) {
@@ -2799,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(
@@ -2831,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);
@@ -2874,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,
@@ -2939,7 +3214,7 @@
}
CharSequence requestingAppName = AppLabelProxy.Util.getAppLabel(
- mContext.getPackageManager(), requestingPackageName);
+ mContext, call.getAssociatedUser(), requestingPackageName, mFeatureFlags);
if (requestingAppName == null) {
requestingAppName = requestingPackageName;
}
@@ -3033,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;
}
/**
@@ -3207,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);
}
/**
@@ -3222,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
@@ -3295,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) {
@@ -3352,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);
}
@@ -3402,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;
@@ -3454,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;
}
@@ -3512,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);
}
}
@@ -3540,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.
*/
@@ -3551,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
@@ -3601,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());
@@ -3674,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");
}
@@ -3714,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));
}
@@ -3736,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.
@@ -3769,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);
@@ -3786,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) {
@@ -3797,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;
@@ -3814,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).
@@ -3828,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 &&
@@ -3838,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();
}
}
@@ -3946,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 {
@@ -3972,6 +4454,7 @@
return true;
}
}
+ Log.i(this, "onMediaButton: type=%d; no active calls", type);
return false;
}
@@ -4063,6 +4546,10 @@
return getFirstCallWithState(null, states);
}
+ public Call getFirstCallWithLiveState() {
+ return getFirstCallWithState(null, LIVE_CALL_STATES);
+ }
+
@VisibleForTesting
public PhoneNumberUtilsAdapter getPhoneNumberUtilsAdapter() {
return mPhoneNumberUtilsAdapter;
@@ -4153,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
@@ -4171,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();
@@ -4260,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);
@@ -4277,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) {
@@ -4317,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() {
@@ -4389,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");
}
@@ -4408,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();
- }
}
}
}
@@ -4457,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);
@@ -4478,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);
@@ -4505,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
@@ -4514,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);
}
@@ -4548,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
@@ -4677,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);
}
@@ -4737,15 +5191,22 @@
/**
* Determines the number of unholdable calls present in a connection service other than the one
- * the passed phone account belonds to.
+ * the passed phone account belongs to. If a ConnectionService has not been associated with an
+ * outgoing call yet (for example, it is in the SELECT_PHONE_ACCOUNT state), then we do not
+ * count that call because it is not tracked as an active call yet.
* @param phoneAccountHandle The handle of the PhoneAccount.
* @return Number of unholdable calls owned by other connection service.
*/
public int getNumUnholdableCallsForOtherConnectionService(
PhoneAccountHandle phoneAccountHandle) {
return (int) mCalls.stream().filter(call ->
- !phoneAccountHandle.getComponentName().equals(
- call.getTargetPhoneAccount().getComponentName())
+ // If this convention needs to be changed, answerCall will need to be modified to
+ // 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
&& !call.isExternalCall()
&& !canHold(call)).count();
@@ -4814,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
@@ -4890,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);
}
@@ -4976,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!
@@ -5006,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.");
@@ -5203,7 +5688,8 @@
connection.getConnectTimeMillis() /* connectTimeMillis */,
connection.getConnectElapsedTimeMillis(), /* connectElapsedTimeMillis */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
call.initAnalytics();
call.getAnalytics().setCreatedFromExistingConnection(true);
@@ -5218,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());
@@ -5301,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());
+ }
}
}
@@ -5313,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);
}
}
@@ -5422,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",
@@ -5443,7 +5945,7 @@
mCallAudioManager.getCallAudioModeStateMachine().getHandler().post(() -> {
mainHandlerLatch.countDown();
});
- mCallAudioManager.getCallAudioRouteStateMachine().getHandler().post(() -> {
+ mCallAudioManager.getCallAudioRouteAdapter().getAdapterHandler().post(() -> {
mainHandlerLatch.countDown();
});
@@ -5469,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;
}
}
@@ -5490,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;
+ }
}
}
@@ -5793,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}.
@@ -5860,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
@@ -6067,7 +6550,8 @@
false /* forceAttachToExistingConnection */,
false, /* isConference */
mClockProxy,
- mToastFactory);
+ mToastFactory,
+ mFeatureFlags);
if (fromCall == null || isHandoverInProgress() ||
!isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount()) ||
@@ -6136,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
old mode 100755
new mode 100644
index bcef305..260c238
--- 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;
@@ -45,8 +44,8 @@
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
-import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
+import android.telecom.Logging.Runnable;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccountHandle;
@@ -66,14 +65,16 @@
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;
import java.util.Map;
import java.util.Set;
-import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
@@ -81,6 +82,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import java.util.Objects;
/**
* Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps
@@ -90,18 +92,31 @@
*/
@VisibleForTesting
public class ConnectionServiceWrapper extends ServiceBinder implements
- ConnectionServiceFocusManager.ConnectionServiceFocus {
+ ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
+
+ /**
+ * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+ */
+ public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
+ UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
+ public static final String CREATE_CONNECTION_TIMEOUT_ERROR_MSG =
+ "Timeout expired before Telecom connection was created.";
+ public static final UUID CREATE_CONFERENCE_TIMEOUT_ERROR_UUID =
+ UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
+ public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
+ "Timeout expired before Telecom conference was created.";
private static final String TELECOM_ABBREVIATION = "cast";
+ private static final long SERVICE_BINDING_TIMEOUT = 15000L;
private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
private @Nullable CancellationSignal mOngoingQueryLocationRequest = null;
private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor();
-
- private static final long SERVICE_BINDING_TIMEOUT = 15000L;
private ScheduledExecutorService mScheduledExecutor =
Executors.newSingleThreadScheduledExecutor();
// Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
+ private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+
private final class Adapter extends IConnectionServiceAdapter.Stub {
@Override
@@ -115,11 +130,7 @@
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
Call call = mCallIdMapper.getCall(callId);
- if (mScheduledFutureMap.containsKey(call)) {
- ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
- existingTimeout.cancel(false /* cancelIfRunning */);
- mScheduledFutureMap.remove(call);
- }
+ maybeRemoveCleanupFuture(call);
// Check status hints image for cross user access
if (connection.getStatusHints() != null) {
Icon icon = connection.getStatusHints().getIcon();
@@ -158,18 +169,14 @@
try {
synchronized (mLock) {
logIncoming("handleCreateConferenceComplete %s", callId);
+ Call call = mCallIdMapper.getCall(callId);
+ maybeRemoveCleanupFuture(call);
// Check status hints image for cross user access
if (conference.getStatusHints() != null) {
Icon icon = conference.getStatusHints().getIcon();
conference.getStatusHints().setIcon(StatusHints.
validateAccountIconUserBoundary(icon, callingUserHandle));
}
- Call call = mCallIdMapper.getCall(callId);
- if (mScheduledFutureMap.containsKey(call)) {
- ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
- existingTimeout.cancel(false /* cancelIfRunning */);
- mScheduledFutureMap.remove(call);
- }
ConnectionServiceWrapper.this
.handleCreateConferenceComplete(callId, request, conference);
@@ -407,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));
}
@@ -543,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
@@ -1019,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
@@ -1057,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);
@@ -1398,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
@@ -1433,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;
}
@@ -1622,22 +1637,24 @@
.setParticipants(call.getParticipants())
.setIsAdhocConferenceCall(call.isAdhocConferenceCall())
.build();
-
Runnable r = new Runnable("CSW.cC", mLock) {
- @Override
- public void loggedRun() {
- if (!call.isCreateConnectionComplete()) {
- Log.e(this, new Exception(),
- "Conference %s creation timeout",
- getComponentName());
- Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
- Log.piiHandle(call.getHandle()) + " via:" +
- getComponentName().getPackageName());
- response.handleCreateConferenceFailure(
- new DisconnectCause(DisconnectCause.ERROR));
- }
- }
- };
+ @Override
+ public void loggedRun() {
+ if (!call.isCreateConnectionComplete()) {
+ Log.e(this, new Exception(),
+ "Conference %s creation timeout",
+ getComponentName());
+ Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
+ Log.piiHandle(call.getHandle()) + " via:" +
+ getComponentName().getPackageName());
+ mAnomalyReporter.reportAnomaly(
+ CREATE_CONFERENCE_TIMEOUT_ERROR_UUID,
+ CREATE_CONFERENCE_TIMEOUT_ERROR_MSG);
+ response.handleCreateConferenceFailure(
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+ }
+ };
// Post cleanup to the executor service and cache the future, so we can cancel it if
// needed.
ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
@@ -1651,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()));
}
@@ -1684,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;
@@ -1744,38 +1766,51 @@
.setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
.setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
.build();
-
Runnable r = new Runnable("CSW.cC", mLock) {
- @Override
- public void loggedRun() {
- if (!call.isCreateConnectionComplete()) {
- Log.e(this, new Exception(),
- "Connection %s creation timeout",
- getComponentName());
- Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
- Log.piiHandle(call.getHandle()) + " via:" +
- getComponentName().getPackageName());
- response.handleCreateConnectionFailure(
- new DisconnectCause(DisconnectCause.ERROR));
- }
- }
- };
+ @Override
+ public void loggedRun() {
+ if (!call.isCreateConnectionComplete()) {
+ Log.e(this, new Exception(),
+ "Connection %s creation timeout",
+ getComponentName());
+ Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
+ Log.piiHandle(call.getHandle()) + " via:" +
+ getComponentName().getPackageName());
+ mAnomalyReporter.reportAnomaly(
+ CREATE_CONNECTION_TIMEOUT_ERROR_UUID,
+ CREATE_CONNECTION_TIMEOUT_ERROR_MSG);
+ response.handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.ERROR));
+ }
+ }
+ };
// Post cleanup to the executor service and cache the future, so we can cancel it if
// needed.
ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
mScheduledFutureMap.put(call, future);
try {
- mServiceInterface.createConnection(
- call.getConnectionManagerPhoneAccount(),
- 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()));
}
@@ -2020,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")) {
@@ -2035,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);
@@ -2051,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")) {
@@ -2095,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 {
@@ -2246,6 +2290,9 @@
if (response != null) {
response.handleCreateConnectionFailure(disconnectCause);
}
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(mCallIdMapper.getCall(callId));
+ }
mCallIdMapper.removeCall(callId);
}
@@ -2255,6 +2302,9 @@
if (response != null) {
response.handleCreateConnectionFailure(disconnectCause);
}
+ if (mFlags.dontTimeoutDestroyedCalls()) {
+ maybeRemoveCleanupFuture(call);
+ }
mCallIdMapper.removeCall(call);
}
@@ -2345,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 {
@@ -2439,6 +2490,13 @@
@Override
protected void removeServiceInterface() {
Log.v(this, "Removing Connection Service Adapter.");
+ if (mServiceInterface == null) {
+ // In some cases, we may receive multiple calls to
+ // remoteServiceInterface, such as when the remote process crashes
+ // (onBinderDied & onServiceDisconnected)
+ Log.w(this, "removeServiceInterface: mServiceInterface is null");
+ return;
+ }
removeConnectionServiceAdapter(mAdapter);
// We have lost our service connection. Notify the world that this service is done.
// We must notify the adapter before CallsManager. The adapter will force any pending
@@ -2447,6 +2505,10 @@
handleConnectionServiceDeath();
mCallsManager.handleConnectionServiceDeath(this);
mServiceInterface = null;
+ if (mScheduledExecutor != null) {
+ mScheduledExecutor.shutdown();
+ mScheduledExecutor = null;
+ }
}
@Override
@@ -2459,6 +2521,7 @@
BindCallback callback = new BindCallback() {
@Override
public void onSuccess() {
+ if (!isServiceValid("connectionServiceFocusLost")) return;
try {
mServiceInterface.connectionServiceFocusLost(
Log.getExternalSession(TELECOM_ABBREVIATION));
@@ -2478,6 +2541,7 @@
BindCallback callback = new BindCallback() {
@Override
public void onSuccess() {
+ if (!isServiceValid("connectionServiceFocusGained")) return;
try {
mServiceInterface.connectionServiceFocusGained(
Log.getExternalSession(TELECOM_ABBREVIATION));
@@ -2556,15 +2620,15 @@
*/
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();
+ mScheduledFutureMap.clear();
if (mConnSvrFocusListener != null) {
mConnSvrFocusListener.onConnectionServiceDeath(this);
@@ -2615,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;
}
@@ -2625,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;
@@ -2695,4 +2759,25 @@
public void setScheduledExecutorService(ScheduledExecutorService service) {
mScheduledExecutor = service;
}
+
+ @VisibleForTesting
+ 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 19691c1..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,7 +267,25 @@
mConnectionAttempt++;
mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
- mCall.setConnectionService(mService);
+ if (mFlags.updatedRcsCallCountTracking()) {
+ 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) {
+ 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()) {
if (mCall.isAdhocConferenceCall()) {
@@ -294,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 1710604..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);
+ 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 7274993..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,10 +309,16 @@
}
final void decrementAssociatedCallCount() {
- decrementAssociatedCallCount(false /*isSuppressingUnbind*/);
+ 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,
@@ -323,6 +333,21 @@
}
}
+ final void decrementAssociatedCallCountUpdated() {
+ if (mAssociatedCallCount > 0) {
+ mAssociatedCallCount--;
+ Log.i(this, "Call count decrement %d, %s", mAssociatedCallCount,
+ mComponentName.flattenToShortString());
+
+ if (mAssociatedCallCount == 0) {
+ unbind();
+ }
+ } else {
+ Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero",
+ mComponentName.getClassName());
+ }
+ }
+
final int getAssociatedCallCount() {
return mAssociatedCallCount;
}
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 f33b185..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,21 +3241,23 @@
/**
* 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) {
int packageUid = -1;
int callingUid = Binder.getCallingUid();
PackageManager pm;
- try{
+ long token = Binder.clearCallingIdentity();
+ 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);
}
if (pm != null) {
try {
@@ -2726,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);
}
@@ -2817,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 "
@@ -2829,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.");
}
}
@@ -2862,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) {
@@ -2961,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.
@@ -2988,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;
}
@@ -3092,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);
}
@@ -3126,7 +3681,7 @@
}
private void broadcastCallScreeningAppChangedIntent(String componentName,
- boolean isDefault) {
+ boolean isDefault) {
if (TextUtils.isEmpty(componentName)) {
return;
}
@@ -3135,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);
}
@@ -3162,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 67bb81f..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,10 +224,15 @@
RoleManagerAdapter roleManagerAdapter,
ContactsAsyncHelper.Factory contactsAsyncHelperFactory,
DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ String sysUiPackageName,
Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
Executor asyncTaskExecutor,
- BlockedNumbersAdapter blockedNumbersAdapter) {
+ Executor asyncCallAudioTaskExecutor,
+ 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);
@@ -238,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() {
@@ -249,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);
@@ -263,7 +277,8 @@
mMissedCallNotifier = missedCallNotifierImplFactory
.makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar,
defaultDialerCache,
- deviceIdleControllerAdapter);
+ deviceIdleControllerAdapter,
+ featureFlags);
DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory =
new DisconnectedCallNotifier.Default();
@@ -272,7 +287,7 @@
mContactsAsyncHelper, mLock);
EmergencyCallHelper emergencyCallHelper = new EmergencyCallHelper(mContext,
- defaultDialerCache, timeoutsAdapter);
+ defaultDialerCache, timeoutsAdapter, mFeatureFlags);
InCallControllerFactory inCallControllerFactory = new InCallControllerFactory() {
@Override
@@ -282,7 +297,7 @@
EmergencyCallHelper emergencyCallHelper) {
return new InCallController(context, lock, callsManager, systemStateProvider,
defaultDialerCache, timeoutsAdapter, emergencyCallHelper,
- new CarModeTracker(), clockProxy);
+ new CarModeTracker(), clockProxy, featureFlags);
}
};
@@ -291,7 +306,7 @@
@Override
public CallEndpointController create(Context context, SyncRoot lock,
CallsManager callsManager) {
- return new CallEndpointController(context, callsManager);
+ return new CallEndpointController(context, callsManager, featureFlags);
}
};
@@ -333,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();
+ }
}
};
@@ -351,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,
@@ -396,10 +424,17 @@
callAnomalyWatchdog,
accessibilityManagerAdapter,
asyncTaskExecutor,
+ asyncCallAudioTaskExecutor,
blockedNumbersAdapter,
transactionManager,
emergencyCallDiagnosticLogger,
- callStreamingNotification);
+ communicationDeviceTracker,
+ callStreamingNotification,
+ bluetoothDeviceManager,
+ featureFlags,
+ telephonyFlags,
+ IncomingCallFilterGraph::new,
+ metricsController);
mIncomingCallNotifier = incomingCallNotifier;
incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
@@ -443,7 +478,7 @@
}
mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager,
- defaultDialerCache);
+ defaultDialerCache, featureFlags);
mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
mContext, mCallsManager);
@@ -453,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),
@@ -461,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();
}
@@ -502,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 a304401..57906d4 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -16,10 +16,19 @@
package com.android.server.telecom;
+import android.app.admin.DevicePolicyManager;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.UserInfo;
+import android.net.Uri;
import android.os.UserHandle;
import android.os.UserManager;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.server.telecom.components.ErrorDialogActivity;
+import com.android.server.telecom.flags.FeatureFlags;
public final class UserUtil {
@@ -27,17 +36,124 @@
}
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,
+ int stringId, String tag, String reason) {
+ final Intent intent = new Intent(context, ErrorDialogActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId);
+ context.startActivityAsUser(intent, UserHandle.CURRENT);
+ Log.w(tag, "Rejecting non-emergency phone call because "
+ + reason);
+ }
+
+ public static boolean hasOutgoingCallsUserRestriction(Context context,
+ UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag,
+ FeatureFlags featureFlags) {
+ // Set handle for conference calls. Refer to {@link Connection#ADHOC_CONFERENCE_ADDRESS}.
+ if (handle == null) {
+ handle = Uri.parse("tel:conf-factory");
+ }
+
+ if(!isSelfManaged) {
+ // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
+ // check in a managed profile user because this check can always be bypassed
+ // by copying and pasting the phone number into the personal dialer.
+ if (!UserUtil.isManagedProfile(context, userHandle, 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)) {
+ 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 (hasUserRestriction) {
+ final DevicePolicyManager dpm =
+ context.getSystemService(DevicePolicyManager.class);
+ if (dpm == null) {
+ return true;
+ }
+ final Intent adminSupportIntent = dpm.createAdminSupportIntent(
+ UserManager.DISALLOW_OUTGOING_CALLS);
+ if (adminSupportIntent != null) {
+ context.startActivityAsUser(adminSupportIntent, userHandle);
+ }
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the associated user for the given call. Note: this is applicable to all calls except
+ * outgoing calls as the associated user is already based off of the user placing the
+ * call.
+ *
+ * @param phoneAccountRegistrar
+ * @param currentUser Current user profile (this can either be the admin or a secondary/guest
+ * user). Note that work profile users fall under the admin user.
+ * @param targetPhoneAccount The phone account to retrieve the {@link UserHandle} from.
+ * @return current user if it isn't the admin or if the work profile is paused for the target
+ * phone account handle user, otherwise return the target phone account handle user. If the
+ * flag is disabled, return the legacy {@link UserHandle}.
+ */
+ public static UserHandle getAssociatedUserForCall(boolean isAssociatedUserFlagEnabled,
+ PhoneAccountRegistrar phoneAccountRegistrar, UserHandle currentUser,
+ PhoneAccountHandle targetPhoneAccount) {
+ if (!isAssociatedUserFlagEnabled) {
+ return targetPhoneAccount.getUserHandle();
+ }
+ // For multi-user phone accounts, associate the call with the profile receiving/placing
+ // the call. For SIM accounts (that are assigned to specific users), the user association
+ // will be placed on the target phone account handle user.
+ PhoneAccount account = phoneAccountRegistrar.getPhoneAccountUnchecked(targetPhoneAccount);
+ if (account != null) {
+ return account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)
+ ? currentUser
+ : targetPhoneAccount.getUserHandle();
+ }
+ // If target phone account handle is null or account cannot be found,
+ // return the current user.
+ return currentUser;
}
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 473e7b9..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,24 +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 {
@@ -50,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
@@ -58,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;
@@ -70,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;
@@ -85,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) {
@@ -98,12 +134,15 @@
+ mBluetoothHearingAid;
} else if (profile == BluetoothProfile.LE_AUDIO) {
mBluetoothLeAudioService = (BluetoothLeAudio) proxy;
- logString = "Got BluetoothLeAudio: "
- + mBluetoothLeAudioService;
+ logString = ("Got BluetoothLeAudio: " + mBluetoothLeAudioService )
+ + (", mLeAudioCallbackRegistered: "
+ + mLeAudioCallbackRegistered);
if (!mLeAudioCallbackRegistered) {
- mBluetoothLeAudioService.registerCallback(
- mExecutor, mLeAudioCallbacks);
- mLeAudioCallbackRegistered = true;
+ if (mFeatureFlags.postponeRegisterToLeaudio()) {
+ mExecutor.execute(this::registerToLeAudio);
+ } else {
+ registerToLeAudio();
+ }
}
} else {
logString = "Connected to non-requested bluetooth service." +
@@ -117,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,
@@ -151,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 {
@@ -164,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 =
@@ -174,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);
@@ -183,6 +286,7 @@
private BluetoothRouteManager mBluetoothRouteManager;
private BluetoothHeadset mBluetoothHeadset;
+ private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
private BluetoothHearingAid mBluetoothHearingAid;
private boolean mLeAudioCallbackRegistered = false;
private BluetoothLeAudio mBluetoothLeAudioService;
@@ -194,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,
@@ -204,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) {
@@ -234,18 +348,38 @@
}
public int getNumConnectedDevices() {
- synchronized (mLock) {
- return mHfpDevicesByAddress.size() +
- mHearingAidDevicesByAddress.size() +
- getLeAudioConnectedDevices().size();
- }
+ return getConnectedDevices().size();
}
public Collection<BluetoothDevice> getConnectedDevices() {
synchronized (mLock) {
- ArrayList<BluetoothDevice> result = new ArrayList<>(mHfpDevicesByAddress.values());
+ ArraySet<BluetoothDevice> result = new ArraySet<>();
+
+ // Set storing the group ids of all dual mode audio devices to de-dupe them
+ Set<Integer> dualModeGroupIds = new ArraySet<>();
+ for (BluetoothDevice hfpDevice: mHfpDevicesByAddress.values()) {
+ result.add(hfpDevice);
+ if (mBluetoothLeAudioService == null) {
+ continue;
+ }
+ int groupId = mBluetoothLeAudioService.getGroupId(hfpDevice);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+ dualModeGroupIds.add(groupId);
+ }
+ }
+
result.addAll(mHearingAidDevicesByAddress.values());
- result.addAll(getLeAudioConnectedDevices());
+ if (mBluetoothLeAudioService == null) {
+ return Collections.unmodifiableCollection(result);
+ }
+ for (BluetoothDevice leAudioDevice: getLeAudioConnectedDevices()) {
+ // Exclude dual mode audio devices included from the HFP devices list
+ int groupId = mBluetoothLeAudioService.getGroupId(leAudioDevice);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID
+ && !dualModeGroupIds.contains(groupId)) {
+ result.add(leAudioDevice);
+ }
+ }
return Collections.unmodifiableCollection(result);
}
}
@@ -253,9 +387,9 @@
// Same as getConnectedDevices except it filters out the hearing aid devices that are linked
// together by their hiSyncId.
public Collection<BluetoothDevice> getUniqueConnectedDevices() {
- ArrayList<BluetoothDevice> result;
+ ArraySet<BluetoothDevice> result;
synchronized (mLock) {
- result = new ArrayList<>(mHfpDevicesByAddress.values());
+ result = new ArraySet<>(mHfpDevicesByAddress.values());
}
Set<Long> seenHiSyncIds = new LinkedHashSet<>();
// Add the left-most active device to the seen list so that we match up with the list
@@ -289,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() {
@@ -330,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. */
@@ -349,30 +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, "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);
@@ -386,29 +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, "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() {
@@ -440,6 +613,7 @@
if (audioDeviceInfo != null && audioDeviceInfo.getType()
== AudioDeviceInfo.TYPE_BLE_HEADSET) {
mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
+ Log.i(this, "clearLeAudioCommunicationDevice: audioManager#clearCommunicationDevice");
mAudioManager.clearCommunicationDevice();
}
}
@@ -464,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;
@@ -497,7 +672,7 @@
}
if (bleHeadset == null) {
- Log.w(this, " No bleHeadset device available");
+ Log.w(this, "setLeAudioCommunicationDevice: No bleHeadset device available");
return false;
}
@@ -507,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();
@@ -518,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;
@@ -546,7 +722,7 @@
}
if (hearingAid == null) {
- Log.w(this, " No hearingAid device available");
+ Log.w(this, "setHearingAidCommunicationDevice: No hearingAid device available");
return false;
}
@@ -556,73 +732,198 @@
// 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;
+ BluetoothDevice device = null;
if (mLeAudioDevicesByAddress.containsKey(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;
}
- BluetoothDevice device = mLeAudioDevicesByAddress.get(address);
- if (mBluetoothAdapter.setActiveDevice(
- device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
-
- /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
- * 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 true;
- }
- return false;
+ device = mLeAudioDevicesByAddress.get(address);
+ callProfile = BluetoothProfile.LE_AUDIO;
} else if (mHearingAidDevicesByAddress.containsKey(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;
}
- if (mBluetoothAdapter.setActiveDevice(
- mHearingAidDevicesByAddress.get(address),
- BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
+ 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)) {
+ 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: %s", address);
+ return false;
+ }
+
+ 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=% is %d", address,
+ preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+ callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ }
+
+ if (callProfile == BluetoothProfile.LE_AUDIO) {
+ if (mBluetoothAdapter.setActiveDevice(
+ device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
/* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
* 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_BLE_HEADSET, device)
+ : setLeAudioCommunicationDevice();
}
-
return true;
}
return false;
- } else if (mHfpDevicesByAddress.containsKey(address)) {
- BluetoothDevice device = mHfpDevicesByAddress.get(address);
- if (mBluetoothHeadset == null) {
- Log.w(this, "Attempting to turn on audio when the headset service is null");
- return false;
+ } else if (callProfile == BluetoothProfile.HEARING_AID) {
+ if (mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
+ /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
+ * 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 mFeatureFlags.callAudioCommunicationDeviceRefactor() ?
+ mCommunicationDeviceTracker.setCommunicationDevice(
+ AudioDeviceInfo.TYPE_HEARING_AID, null)
+ : setHearingAidCommunicationDevice();
+ }
+ return true;
}
+ return false;
+ } else if (callProfile == BluetoothProfile.HEADSET) {
boolean success = mBluetoothAdapter.setActiveDevice(device,
BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
if (!success) {
- Log.w(this, "Couldn't set active device to %s", address);
+ 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;
}
}
@@ -642,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 7966f73..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -17,15 +17,18 @@
package com.android.server.telecom.bluetooth;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothLeAudio;
import android.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;
@@ -33,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;
@@ -78,6 +82,7 @@
void onBluetoothActiveDevicePresent();
void onBluetoothActiveDeviceGone();
void onBluetoothAudioConnected();
+ void onBluetoothAudioConnecting();
void onBluetoothAudioDisconnected();
/**
* This gets called when we get an unexpected state change from Bluetooth. Their stack does
@@ -131,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
@@ -160,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 {
@@ -176,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 {
@@ -231,8 +264,7 @@
sendMessageDelayed(CONNECTION_TIMEOUT, args,
mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
mContext.getContentResolver()));
- // Pretend like audio is connected when communicating w/ CARSM.
- mListener.onBluetoothAudioConnected();
+ mListener.onBluetoothAudioConnecting();
}
@Override
@@ -250,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:
@@ -263,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 {
@@ -282,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 {
@@ -368,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:
@@ -380,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
@@ -388,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);
@@ -402,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 {
@@ -469,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);
@@ -621,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;
@@ -645,6 +793,10 @@
}
}
+ public BluetoothDevice getMostRecentlyReportedActiveDevice() {
+ return mMostRecentlyReportedActiveDevice;
+ }
+
public boolean hasBtActiveDevice() {
return mLeAudioActiveDeviceCache != null ||
mHearingAidActiveDeviceCache != null ||
@@ -663,8 +815,151 @@
return mDeviceManager.getUniqueConnectedDevices();
}
- private String connectBtAudio(String address, boolean switchingBtDevices) {
- return connectBtAudio(address, 0, switchingBtDevices);
+ public boolean isWatch(BluetoothDevice device) {
+ if (device == null) {
+ Log.i(this, "isWatch: device is null. Returning false");
+ return false;
+ }
+
+ BluetoothClass deviceClass = device.getBluetoothClass();
+ if (deviceClass != null && deviceClass.getDeviceClass()
+ == BluetoothClass.Device.WEARABLE_WRIST_WATCH) {
+ Log.i(this, "isWatch: bluetooth class component is a WEARABLE_WRIST_WATCH.");
+ return true;
+ }
+
+ // Check metadata
+ byte[] deviceType = device.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE);
+ if (deviceType == null) {
+ return false;
+ }
+ String deviceTypeStr = new String(deviceType);
+ if (deviceTypeStr.equals(BluetoothDevice.DEVICE_TYPE_WATCH)) {
+ Log.i(this, "isWatch: bluetooth device type is DEVICE_TYPE_WATCH.");
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 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);
}
/**
@@ -677,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))
@@ -692,10 +988,19 @@
? address : getActiveDeviceAddress();
if (actualAddress == null) {
Log.i(this, "No device specified and BT stack has no active device."
- + " Using arbitrary device");
+ + " Using arbitrary device - except watch");
if (deviceList.size() > 0) {
- actualAddress = deviceList.iterator().next().getAddress();
- } else {
+ for (BluetoothDevice device : deviceList) {
+ if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
+ Log.i(this, "Skipping a watch device: " + device);
+ continue;
+ }
+ actualAddress = device.getAddress();
+ break;
+ }
+ }
+
+ if (actualAddress == null) {
Log.i(this, "No devices available at all. Not connecting.");
return null;
}
@@ -797,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) {
@@ -853,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);
@@ -939,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 20af7b5..679db67 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,19 @@
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;
import android.bluetooth.BluetoothHearingAid;
@@ -25,14 +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();
@@ -54,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");
@@ -84,7 +105,7 @@
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
BluetoothDevice device =
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
if (device == null) {
Log.w(LOG_TAG, "Got null device from broadcast. " +
"Ignoring.");
@@ -99,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;
}
}
@@ -115,7 +179,7 @@
int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_DISCONNECTED);
BluetoothDevice device =
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
if (device == null) {
Log.w(LOG_TAG, "Got null device from broadcast. " +
@@ -124,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;
@@ -140,24 +208,44 @@
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);
+ }
}
}
private void handleActiveDeviceChanged(Intent intent) {
BluetoothDevice device =
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ 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;
@@ -166,42 +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;
+ // 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);
+ }
}
- args.arg2 = device.getAddress();
-
- 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 (!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);
+ }
}
}
- }
+ }
}
}
@@ -210,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 36f2077..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,12 +193,13 @@
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)
.setShouldAddToCallLog(true)
.setShouldShowNotification(false)
+ .setShouldSilence(true)
.setCallBlockReason(getBlockReason(blockStatus))
.setCallScreeningAppName(null)
.setCallScreeningComponentName(null)
@@ -142,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;
@@ -151,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:
@@ -173,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 ef85fc7..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(
@@ -215,12 +222,16 @@
}
},
Executors.newCachedThreadPool(),
+ Executors.newSingleThreadExecutor(),
new BlockedNumbersAdapter() {
@Override
public boolean shouldShowEmergencyCallNotification(Context
context) {
- return BlockedNumberContract.SystemContract
- .shouldShowEmergencyCallNotification(context);
+ return featureFlags.telecomMainlineBlockedNumbersManager()
+ ? context.getSystemService(BlockedNumbersManager.class)
+ .shouldShowEmergencyCallNotification()
+ : BlockedNumberContract.SystemContract
+ .shouldShowEmergencyCallNotification(context);
}
@Override
@@ -229,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 a4602c1..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,47 +109,17 @@
handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null);
}
- if(!isSelfManaged) {
- // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
- // check in a managed profile user because this check can always be bypassed
- // by copying and pasting the phone number into the personal dialer.
- if (!UserUtil.isManagedProfile(mContext, mUserHandle)) {
- // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
- // restriction.
- if (!TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
- final UserManager userManager =
- (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
- mUserHandle)) {
- showErrorDialogForRestrictedOutgoingCall(mContext,
- R.string.outgoing_call_not_allowed_user_restriction);
- Log.w(this, "Rejecting non-emergency phone call "
- + "due to DISALLOW_OUTGOING_CALLS restriction");
- return;
- } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
- mUserHandle)) {
- final DevicePolicyManager dpm =
- mContext.getSystemService(DevicePolicyManager.class);
- if (dpm == null) {
- return;
- }
- final Intent adminSupportIntent = dpm.createAdminSupportIntent(
- UserManager.DISALLOW_OUTGOING_CALLS);
- if (adminSupportIntent != null) {
- mContext.startActivity(adminSupportIntent);
- }
- return;
- }
- }
- }
- }
+ if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle, handle, isSelfManaged,
+ UserCallIntentProcessor.class.getCanonicalName(), mFeatureFlags)) {
+ return;
+ }
if (!isSelfManaged && !canCallNonEmergency &&
!TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
- showErrorDialogForRestrictedOutgoingCall(mContext,
- R.string.outgoing_call_not_allowed_no_permission);
- Log.w(this, "Rejecting non-emergency phone call because "
- + android.Manifest.permission.CALL_PHONE + " permission is not granted.");
+ String reason = android.Manifest.permission.CALL_PHONE + " permission is not granted.";
+ UserUtil.showErrorDialogForRestrictedOutgoingCall(mContext,
+ R.string.outgoing_call_not_allowed_no_permission,
+ this.getClass().getCanonicalName(), reason);
return;
}
@@ -187,11 +161,4 @@
}
return true;
}
-
- private static void showErrorDialogForRestrictedOutgoingCall(Context context, int stringId) {
- final Intent intent = new Intent(context, ErrorDialogActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId);
- context.startActivityAsUser(intent, UserHandle.CURRENT);
- }
}
diff --git a/src/com/android/server/telecom/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-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 7df4f29..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;
@@ -72,6 +72,7 @@
private static final CallFilteringResult BLOCK_RESULT = new CallFilteringResult.Builder()
.setShouldAllowCall(false)
.setShouldReject(true)
+ .setShouldSilence(true)
.setShouldAddToCallLog(true)
.setShouldShowNotification(false)
.setCallBlockReason(CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER)
@@ -87,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);
@@ -106,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);
@@ -120,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);
@@ -132,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 c37d136..ac4a94e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -18,19 +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;
@@ -43,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 {
@@ -72,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;
@@ -103,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);
@@ -114,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);
@@ -125,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
@@ -178,6 +193,7 @@
buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+ when(mBluetoothLeAudio.getGroupId(device5)).thenReturn(1);
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device5);
receiverUnderTest.onReceive(mContext,
@@ -188,6 +204,7 @@
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 2);
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(2)).thenReturn(device6);
+ when(mBluetoothLeAudio.getGroupId(device6)).thenReturn(1);
receiverUnderTest.onReceive(mContext,
buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
@@ -263,17 +280,19 @@
@Test
public void testLeAudioDedup() {
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+ buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device1,
BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device5,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device6,
+ buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device6,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 1);
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device5);
+ when(mBluetoothLeAudio.getGroupId(device5)).thenReturn(1);
+ when(mBluetoothLeAudio.getGroupId(device6)).thenReturn(1);
assertEquals(2, mBluetoothDeviceManager.getNumConnectedDevices());
assertEquals(2, mBluetoothDeviceManager.getUniqueConnectedDevices().size());
}
@@ -390,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();
@@ -408,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);
@@ -429,7 +449,7 @@
when(mockAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
mBluetoothDeviceManager.disconnectAudio();
- verify(mockAudioManager).clearCommunicationDevice();
+ verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice();
}
@SmallTest
@@ -443,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);
@@ -458,12 +478,14 @@
verify(mBluetoothHeadset, never()).connectAudio();
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_AUDIO));
receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device5,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
mBluetoothDeviceManager.disconnectAudio();
- verify(mockAudioManager).clearCommunicationDevice();
+ verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice();
}
@SmallTest
@@ -485,6 +507,8 @@
verify(mBluetoothHeadset, never()).connectAudio();
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
when(mAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
.thenReturn(Arrays.asList(device5, device6));
@@ -499,23 +523,219 @@
@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);
+ 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<>();
- devices.add(mockAudioDeviceInfo);
+ 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(eq(mockAudioDeviceInfo)))
+ when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
.thenReturn(true);
- mBluetoothDeviceManager.setHearingAidCommunicationDevice();
- when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
- mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
+ // 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));
- assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
+ // 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
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothLeAudio.STATE_CONNECTED, device6,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 1);
+ // HFP device connected
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
+ when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+ 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);
+ when(mockAudioDevice6Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(mockAudioDevice5Info);
+ devices.add(mockAudioDevice6Info);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(mockAudioDevice5Info))
+ .thenReturn(true);
+
+ Bundle hfpPreferred = new Bundle();
+ hfpPreferred.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.HEADSET);
+ Bundle leAudioPreferred = new Bundle();
+ leAudioPreferred.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.LE_AUDIO);
+
+ // TEST 1: LE Audio preferred for DUPLEX
+ when(mAdapter.getPreferredAudioProfiles(device5)).thenReturn(leAudioPreferred);
+ when(mAdapter.getPreferredAudioProfiles(device6)).thenReturn(leAudioPreferred);
+ mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
+ verify(mAdapter, times(1)).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ verify(mBluetoothHeadset, never()).connectAudio();
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mockAudioManager).setCommunicationDevice(mockAudioDevice5Info);
+
+ when(mAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
+ .thenReturn(Arrays.asList(device5, device6));
+
+ // Check disconnect during a call
+ devices.remove(mockAudioDevice5Info);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeRemoved(device5, 1);
+
+ mBluetoothDeviceManager.connectAudio(device6.getAddress(), false);
+ verify(mAdapter).setActiveDevice(device6, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ 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);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+
+ // Disconnects audio
+ mBluetoothDeviceManager.disconnectAudio();
+ verify(mockAudioManager, times(2)).clearCommunicationDevice();
+ verify(mBluetoothHeadset, times(1)).disconnectAudio();
+
+ // TEST 2: HFP preferred for DUPLEX
+ when(mAdapter.getPreferredAudioProfiles(device5)).thenReturn(hfpPreferred);
+ when(mAdapter.getPreferredAudioProfiles(device6)).thenReturn(hfpPreferred);
+ when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+ 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(mBluetoothHeadset).connectAudio();
+ mBluetoothDeviceManager.disconnectAudio();
+ verify(mBluetoothHeadset, times(2)).disconnectAudio();
+ }
+
+ @SmallTest
+ @Test
+ public void testClearHearingAidCommunicationDeviceLegacy() {
+ assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_HEARING_AID);
+ }
+
+ @SmallTest
+ @Test
+ public void testClearHearingAidCommunicationDeviceWithFlag() {
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_HEARING_AID);
+ }
+
+ @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
@@ -525,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 5eecccc..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,28 +62,13 @@
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 {
DEVICE_LIST_CHANGED, ACTIVE_DEVICE_PRESENT, ACTIVE_DEVICE_GONE,
- AUDIO_CONNECTED, AUDIO_DISCONNECTED, UNEXPECTED_STATE_CHANGE
+ AUDIO_CONNECTING, AUDIO_CONNECTED, AUDIO_DISCONNECTED, UNEXPECTED_STATE_CHANGE
}
private static class BluetoothRouteTestParametersBuilder {
@@ -263,6 +265,7 @@
@Mock private BluetoothLeAudio mBluetoothLeAudio;
@Mock private Timeouts.Adapter mTimeoutsAdapter;
@Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+ @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Override
@Before
@@ -348,6 +351,9 @@
case ACTIVE_DEVICE_GONE:
verify(mListener).onBluetoothActiveDeviceGone();
break;
+ case AUDIO_CONNECTING:
+ verify(mListener).onBluetoothAudioConnecting();
+ break;
case AUDIO_CONNECTED:
verify(mListener).onBluetoothAudioConnected();
break;
@@ -413,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);
@@ -449,7 +456,7 @@
.setConnectedDevices(DEVICE2, DEVICE1)
.setActiveDevice(DEVICE1)
.setMessageType(BluetoothRouteManager.CONNECT_BT)
- .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+ .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
.setExpectedBluetoothInteraction(CONNECT)
.setExpectedConnectionDevice(DEVICE1)
.setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
@@ -505,7 +512,7 @@
.setConnectedDevices(DEVICE2, DEVICE1, DEVICE3)
.setMessageType(BluetoothRouteManager.CONNECT_BT)
.setMessageDevice(DEVICE3)
- .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+ .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
.setExpectedBluetoothInteraction(CONNECT_SWITCH_DEVICE)
.setExpectedConnectionDevice(DEVICE3)
.setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
@@ -519,7 +526,7 @@
.setConnectedDevices(DEVICE2, DEVICE1, DEVICE3)
.setMessageType(BluetoothRouteManager.CONNECT_BT)
.setMessageDevice(DEVICE3)
- .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+ .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
.setExpectedBluetoothInteraction(CONNECT_SWITCH_DEVICE)
.setExpectedConnectionDevice(DEVICE3)
.setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
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 dfe1483..79247be 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
@@ -24,8 +24,9 @@
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;
import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.DockManager;
@@ -47,6 +48,7 @@
@Mock private BluetoothRouteManager mBluetoothRouteManager;
@Mock private WiredHeadsetManager mWiredHeadsetManager;
@Mock private DockManager mDockManager;
+ @Mock private AsyncRingtonePlayer mRingtonePlayer;
@Override
@Before
@@ -57,7 +59,8 @@
mCallAudioRouteStateMachine,
mBluetoothRouteManager,
mWiredHeadsetManager,
- mDockManager);
+ mDockManager,
+ mRingtonePlayer);
}
@Override
@@ -126,6 +129,16 @@
mAdapter.onBluetoothAudioConnected();
verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ verify(mRingtonePlayer).updateBtActiveState(true);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnBluetoothAudioConnecting() {
+ mAdapter.onBluetoothAudioConnecting();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ verify(mRingtonePlayer).updateBtActiveState(false);
}
@SmallTest
@@ -134,6 +147,7 @@
mAdapter.onBluetoothAudioDisconnected();
verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
+ verify(mRingtonePlayer).updateBtActiveState(false);
}
@SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 569c487..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,24 +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.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 {
@@ -94,12 +100,14 @@
@Mock Call fakeSelfManagedCall;
@Mock Call fakeCall;
@Mock CallAudioManager mockCallAudioManager;
+ @Mock BluetoothDevice mockWatchDevice;
private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
private static final int TEST_TIMEOUT = 500;
private AudioManager mockAudioManager;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
private HandlerThread mThreadHandler;
+ CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
@Override
@Before
@@ -110,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
@@ -129,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
@@ -154,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
@@ -174,7 +189,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);
stateMachine.setCallAudioManager(mockCallAudioManager);
Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
@@ -197,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(),
@@ -208,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 +343,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,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -250,6 +375,7 @@
foundValid = true;
}
assertTrue(foundValid);
+ verify(mockBluetoothRouteManager, timeout(1000L)).getBluetoothAudioConnectedDevice();
}
@MediumTest
@@ -264,7 +390,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);
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);
}
@@ -310,7 +438,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);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -331,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(
@@ -339,7 +469,7 @@
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
}
@@ -355,7 +485,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);
Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1);
@@ -380,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
@@ -403,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(
@@ -415,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());
}
@@ -434,7 +566,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);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -449,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));
}
@@ -471,7 +605,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);
setInBandRing(false);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -485,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())
@@ -494,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,
@@ -505,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
@@ -527,7 +666,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);
List<BluetoothDevice> availableDevices =
Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3);
@@ -553,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
@@ -578,7 +782,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);
when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -587,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));
}
@@ -610,7 +816,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);
when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
@@ -622,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
@@ -645,7 +853,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);
List<BluetoothDevice> availableDevices =
Arrays.asList(bluetoothDevice1, bluetoothDevice2);
@@ -667,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.
@@ -678,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);
@@ -761,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());
}
@@ -778,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,
@@ -789,15 +1109,207 @@
CallAudioState expectedEndState = new CallAudioState(false,
CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_STREAMING);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
verifyNewSystemCallAudioState(initState, expectedEndState);
resetMocks();
stateMachine.sendMessageWithSessionInfo(
CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
- waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
assertEquals(initState, stateMachine.getCurrentCallAudioState());
}
+ @SmallTest
+ @Test
+ public void testIgnoreSpeakerOffMessage() {
+ when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true);
+ when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+ .thenReturn(bluetoothDevice1);
+ when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ stateMachine.initialize(initState);
+
+ doAnswer(
+ (address) -> {
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SPEAKER_OFF);
+ stateMachine.sendMessageDelayed(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED,
+ 5000L);
+ return null;
+ }).when(mockBluetoothRouteManager).connectBluetoothAudio(anyString());
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH);
+
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_SPEAKER | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_EARPIECE);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ @MediumTest
+ @Test
+ public void testIgnoreImplicitBTSwitchWhenDeviceIsWatch() {
+ when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+ when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothRouteManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */,
+ mCommunicationDeviceTracker,
+ mFeatureFlags);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ AudioDeviceInfo headset = mock(AudioDeviceInfo.class);
+ when(headset.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(headset.getAddress()).thenReturn("");
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(headset);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(eq(headset)))
+ .thenReturn(true);
+ when(mockAudioManager.getCommunicationDevice()).thenReturn(headset);
+
+ CallAudioState initState = new CallAudioState(false,
+ CallAudioState.ROUTE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET
+ | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+ stateMachine.initialize(initState);
+
+ // Switch to active
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+ // Make sure that we've successfully switched to the active headset.
+ assertTrue(stateMachine.isInActiveState());
+ ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+ AudioDeviceInfo.class);
+ verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+ assertEquals(AudioDeviceInfo.TYPE_WIRED_HEADSET, infoArgumentCaptor.getValue().getType());
+
+ // Set up watch device as only available BT device.
+ Collection<BluetoothDevice> availableDevices = Collections.singleton(mockWatchDevice);
+
+ when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+ when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
+ when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices);
+ when(mockBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+ // Disconnect wired headset to force switch to BT (verify that we ignore the implicit switch
+ // to BT when the watch is the only connected device and that we move into the next
+ // available route.
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
+ null, availableDevices);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ @SmallTest
+ @Test
+ public void testQuiescentBluetoothRouteResetMute() 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(
@@ -816,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());
}
@@ -856,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 9466220..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,12 +64,13 @@
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;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallLogManager;
import com.android.server.telecom.CallState;
@@ -72,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;
@@ -85,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 {
@@ -113,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";
@@ -123,6 +134,11 @@
PhoneAccountRegistrar mMockPhoneAccountRegistrar;
@Mock
MissedCallNotifier mMissedCallNotifier;
+ @Mock
+ AnomalyReporterAdapter mAnomalyReporterAdapter;
+
+ @Mock
+ FeatureFlags mFeatureFlags;
@Override
@Before
@@ -130,7 +146,7 @@
super.setUp();
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
mCallLogManager = new CallLogManager(mContext, mMockPhoneAccountRegistrar,
- mMissedCallNotifier);
+ mMissedCallNotifier, mAnomalyReporterAdapter, mFeatureFlags);
mDefaultAccountHandle = new PhoneAccountHandle(
new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
TEST_PHONE_ACCOUNT_ID,
@@ -161,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
@@ -174,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
@@ -215,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
@@ -318,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
@@ -343,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
@@ -357,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);
@@ -366,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
@@ -382,6 +443,7 @@
VIA_NUMBER_STRING, // viaNumber
null
);
+ when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(false);
mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
CallState.DISCONNECTED);
@@ -390,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
@@ -423,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(
@@ -451,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
@@ -475,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
@@ -502,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
@@ -560,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));
@@ -605,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));
@@ -615,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));
@@ -643,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() {
@@ -668,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));
@@ -698,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));
@@ -710,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
@@ -737,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
@@ -788,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() {
@@ -808,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
@@ -893,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 740f74c..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());
@@ -350,10 +388,19 @@
mAccessibilityManagerAdapter,
// Just do async tasks synchronously to support testing.
command -> command.run(),
+ // For call audio tasks
+ command -> command.run(),
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);
@@ -362,30 +409,49 @@
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(new ComponentName(mContext.getPackageName(),
- mContext.getPackageName().getClass().getName()), mIConnectionService);
+ mComponentContextFixture.addConnectionService(
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
}
@Override
@After
public void tearDown() throws Exception {
mComponentContextFixture.removeConnectionService(
- new ComponentName(mContext.getPackageName(),
- mContext.getPackageName().getClass().getName()),
- mock(IConnectionService.class));
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
super.tearDown();
}
@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 {
@@ -2770,6 +3251,35 @@
assertTrue(result.contains("onReceiveResult"));
}
+ @Test
+ public void testConnectionServiceCreateConnectionTimeout() throws Exception {
+ ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+ SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
+ mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
+ TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
+ service.setScheduledExecutorService(scheduledExecutorService);
+ Call call = addSpyCall();
+ service.addCall(call);
+ when(call.isCreateConnectionComplete()).thenReturn(false);
+ CreateConnectionResponse response = mock(CreateConnectionResponse.class);
+
+ service.createConnection(call, response);
+ waitUntilConditionIsTrueOrTimeout(new Condition() {
+ @Override
+ public Object expected() {
+ return true;
+ }
+
+ @Override
+ public Object actual() {
+ return scheduledExecutorService.isRunnableScheduledAtTime(15000L);
+ }
+ }, 5000L, "Expected job failed to schedule");
+ scheduledExecutorService.advanceTime(15000L);
+ verify(response).handleCreateConnectionFailure(
+ eq(new DisconnectCause(DisconnectCause.ERROR)));
+ }
+
@SmallTest
@Test
public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() {
@@ -2819,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);
@@ -2979,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);
}
@@ -2994,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
@@ -3013,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();
}
@@ -3034,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();
}
@@ -3142,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));
}
@@ -3172,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);
@@ -3263,32 +3755,27 @@
assertTrue(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
}
+ @SmallTest
@Test
- public void testConnectionServiceCreateConnectionTimeout() throws Exception {
- ConnectionServiceWrapper service = new ConnectionServiceWrapper(new ComponentName(
- mContext.getPackageName(), mContext.getPackageName().getClass().getName()), null,
- mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null);
- TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
- service.setScheduledExecutorService(scheduledExecutorService);
- Call call = addSpyCall();
- service.addCall(call);
- when(call.isCreateConnectionComplete()).thenReturn(false);
- CreateConnectionResponse response = mock(CreateConnectionResponse.class);
+ 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);
- service.createConnection(call, response);
- waitUntilConditionIsTrueOrTimeout(new Condition() {
- @Override
- public Object expected() {
- return true;
- }
+ mCallsManager.onCallFilteringComplete(call, result, false);
- @Override
- public Object actual() {
- return scheduledExecutorService.isRunnableScheduledAtTime(15000L);
- }
- }, 5000L, "Expected job failed to schedule");
+ 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);
}
@@ -3318,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;
@@ -3333,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;
}
@@ -3359,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) {
@@ -3376,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);
@@ -3386,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 8a85a87..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,9 +221,9 @@
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();
+ ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
// Make sure the target phone account has the correct permissions
PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
@@ -227,14 +245,52 @@
@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",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
+ mFakeTargetPhoneAccount);
+
+ mTestCreateConnectionProcessor.process();
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(eq(callManagerPAHandle));
+ verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+ // 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:
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ @SmallTest
+ @Test
public void testConnectionManagerFailedFallToSim() throws Exception {
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);
@@ -257,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);
@@ -272,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);
@@ -302,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
@@ -340,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).
*/
@@ -370,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
@@ -407,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.
@@ -447,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
@@ -480,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
@@ -554,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
@@ -692,7 +756,8 @@
/**
* Tests to verify that the
- * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)} can
+ * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)}
+ * can
* successfully sort without running into sort issues related to the hashcodes of the
* PhoneAccounts.
*/
@@ -709,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.
@@ -795,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);
@@ -838,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();
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 f11afc1..df26684 100644
--- a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
@@ -23,28 +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;
@@ -62,15 +63,14 @@
@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;
@Mock private InCallTonePlayer.ToneGeneratorFactory mToneGeneratorFactory;
@Mock private WiredHeadsetManager mWiredHeadsetManager;
@Mock private DockManager mDockManager;
+ @Mock private AsyncRingtonePlayer mRingtonePlayer;
@Mock private BluetoothDevice mDevice;
- @Mock private BluetoothAdapter mBluetoothAdapter;
private InCallTonePlayer.MediaPlayerAdapter mMediaPlayerAdapter =
new InCallTonePlayer.MediaPlayerAdapter() {
@@ -110,7 +110,8 @@
@Mock
private CallAudioManager mCallAudioManager;
-
+ @Mock
+ private Call mCall;
private InCallTonePlayer mInCallTonePlayer;
@Override
@@ -120,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);
+ 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
@@ -145,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
@@ -159,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());
@@ -172,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();
@@ -197,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 a4adf77..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,8 @@
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.verifyNoMoreInteractions;
@@ -43,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;
@@ -50,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;
@@ -62,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;
@@ -110,21 +144,28 @@
@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);
createRingerUnderTest();
}
@@ -136,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);
}
@@ -149,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
@@ -227,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();
@@ -251,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();
@@ -262,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()))
@@ -293,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),
@@ -329,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();
@@ -359,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();
@@ -376,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();
@@ -395,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();
@@ -427,13 +631,55 @@
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();
verify(mockRingtone).play();
}
+ @SmallTest
+ @Test
+ public void testDelayRingerForBtHfpDevices() throws Exception {
+ asyncRingtonePlayer.updateBtActiveState(false);
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
+ ensureRingerIsAudible();
+ assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+ assertTrue(mRingerUnderTest.isRinging());
+ // We should not have the ringtone play until BT moves active
+ verify(mockRingtone, never()).play();
+
+ asyncRingtonePlayer.updateBtActiveState(true);
+ mRingCompletionFuture.get();
+ verify(mockRingtoneFactory, atLeastOnce())
+ .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
+ anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
+ verify(mockRingtone).play();
+
+ mRingerUnderTest.stopRinging();
+ verify(mockRingtone, timeout(1000/*ms*/)).stop();
+ assertFalse(mRingerUnderTest.isRinging());
+ }
+
+ @SmallTest
+ @Test
+ public void testUnblockRingerForStopCommand() throws Exception {
+ asyncRingtonePlayer.updateBtActiveState(false);
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
+ ensureRingerIsAudible();
+ assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+ // We should not have the ringtone play until BT moves active
+ verify(mockRingtone, never()).play();
+
+ // We are not setting BT active, but calling stop ringing while the other thread is waiting
+ // for BT active should also unblock it.
+ mRingerUnderTest.stopRinging();
+ verify(mockRingtone, timeout(1000/*ms*/)).stop();
+ }
+
/**
* test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
* false (meaning DND is ON and the caller cannot bypass the settings)
@@ -446,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));
@@ -455,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));
}
/**
@@ -468,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));
@@ -477,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));
}
/**
@@ -493,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
@@ -503,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));
@@ -518,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));
@@ -538,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));
@@ -572,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
@@ -618,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 b962b2a..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,9 +581,13 @@
ContactsAsyncHelper.ContentResolverAdapter adapter) {
return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
}
- }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
+ }, mDeviceIdleControllerAdapter, SYSTEM_UI_PACKAGE,
+ mAccessibilityManagerAdapter,
Runnable::run,
- mBlockedNumbersAdapter);
+ Runnable::run,
+ mBlockedNumbersAdapter,
+ mFeatureFlags,
+ mTelephonyFlags);
mComponentContextFixture.setTelecomManager(new TelecomManager(
mComponentContextFixture.getTestDouble(),
@@ -583,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());
@@ -767,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);
+ }
}
}