summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2023-10-05 15:43:04 -0700
committerXin Li <delphij@google.com>2023-10-05 15:43:04 -0700
commit7d98325984eba4440d4fbccaa53f5e3ffc67f44b (patch)
tree37b3cb6fe680c2952f211b1ef8b442357610e10f
parent021a0ee8ff8077a27e0bf4efa8d3e3f7bf942d63 (diff)
parentffdcde015742d068069474c40baf815e04cb4af1 (diff)
downloadTelecomm-7d98325984eba4440d4fbccaa53f5e3ffc67f44b.tar.gz
Merge Android 14
Bug: 298295554 Merged-In: I62c6e6b75ab691ca8057f8151acacf6623fe53f9 Change-Id: Ia745868e775c8a786e0a3f666d7f87996431edf9
-rw-r--r--Android.bp4
-rw-r--r--AndroidManifest.xml4
-rw-r--r--PREUPLOAD.cfg5
-rw-r--r--res/drawable/gm_phonelink.xml11
-rw-r--r--res/drawable/person_circle.xml13
-rw-r--r--res/values-af/strings.xml11
-rw-r--r--res/values-am/strings.xml11
-rw-r--r--res/values-ar/strings.xml11
-rw-r--r--res/values-as/strings.xml11
-rw-r--r--res/values-az/strings.xml11
-rw-r--r--res/values-b+sr+Latn/strings.xml11
-rw-r--r--res/values-be/strings.xml11
-rw-r--r--res/values-bg/strings.xml11
-rw-r--r--res/values-bn/strings.xml11
-rw-r--r--res/values-bs/strings.xml11
-rw-r--r--res/values-ca/strings.xml11
-rw-r--r--res/values-cs/strings.xml11
-rw-r--r--res/values-da/strings.xml11
-rw-r--r--res/values-de/strings.xml11
-rw-r--r--res/values-el/strings.xml11
-rw-r--r--res/values-en-rAU/strings.xml11
-rw-r--r--res/values-en-rCA/strings.xml11
-rw-r--r--res/values-en-rGB/strings.xml11
-rw-r--r--res/values-en-rIN/strings.xml11
-rw-r--r--res/values-en-rXC/strings.xml11
-rw-r--r--res/values-es-rUS/strings.xml11
-rw-r--r--res/values-es/strings.xml11
-rw-r--r--res/values-et/strings.xml11
-rw-r--r--res/values-eu/strings.xml11
-rw-r--r--res/values-fa/strings.xml11
-rw-r--r--res/values-fi/strings.xml11
-rw-r--r--res/values-fr-rCA/strings.xml11
-rw-r--r--res/values-fr/strings.xml11
-rw-r--r--res/values-gl/strings.xml11
-rw-r--r--res/values-gu/strings.xml11
-rw-r--r--res/values-hi/strings.xml11
-rw-r--r--res/values-hr/strings.xml11
-rw-r--r--res/values-hu/strings.xml11
-rw-r--r--res/values-hy/strings.xml11
-rw-r--r--res/values-in/strings.xml11
-rw-r--r--res/values-is/strings.xml11
-rw-r--r--res/values-it/strings.xml11
-rw-r--r--res/values-iw/strings.xml11
-rw-r--r--res/values-ja/strings.xml11
-rw-r--r--res/values-ka/strings.xml11
-rw-r--r--res/values-kk/strings.xml11
-rw-r--r--res/values-km/strings.xml11
-rw-r--r--res/values-kn/strings.xml11
-rw-r--r--res/values-ko/strings.xml11
-rw-r--r--res/values-ky/strings.xml11
-rw-r--r--res/values-lo/strings.xml11
-rw-r--r--res/values-lt/strings.xml11
-rw-r--r--res/values-lv/strings.xml11
-rw-r--r--res/values-mk/strings.xml11
-rw-r--r--res/values-ml/strings.xml11
-rw-r--r--res/values-mn/strings.xml11
-rw-r--r--res/values-mr/strings.xml11
-rw-r--r--res/values-ms/strings.xml11
-rw-r--r--res/values-my/strings.xml11
-rw-r--r--res/values-nb/strings.xml11
-rw-r--r--res/values-ne/strings.xml11
-rw-r--r--res/values-nl/strings.xml11
-rw-r--r--res/values-or/strings.xml11
-rw-r--r--res/values-pa/strings.xml11
-rw-r--r--res/values-pl/strings.xml11
-rw-r--r--res/values-pt-rPT/strings.xml15
-rw-r--r--res/values-pt/strings.xml11
-rw-r--r--res/values-ro/strings.xml11
-rw-r--r--res/values-ru/strings.xml11
-rw-r--r--res/values-si/strings.xml11
-rw-r--r--res/values-sk/strings.xml11
-rw-r--r--res/values-sl/strings.xml11
-rw-r--r--res/values-sq/strings.xml11
-rw-r--r--res/values-sr/strings.xml11
-rw-r--r--res/values-sv/strings.xml11
-rw-r--r--res/values-sw/strings.xml11
-rw-r--r--res/values-ta/strings.xml11
-rw-r--r--res/values-te/strings.xml11
-rw-r--r--res/values-th/strings.xml11
-rw-r--r--res/values-tl/strings.xml11
-rw-r--r--res/values-tr/strings.xml11
-rw-r--r--res/values-uk/strings.xml11
-rw-r--r--res/values-ur/strings.xml11
-rw-r--r--res/values-uz/strings.xml11
-rw-r--r--res/values-vi/strings.xml11
-rw-r--r--res/values-zh-rCN/strings.xml15
-rw-r--r--res/values-zh-rHK/strings.xml11
-rw-r--r--res/values-zh-rTW/strings.xml11
-rw-r--r--res/values-zu/strings.xml11
-rw-r--r--res/values/strings.xml34
-rw-r--r--src/com/android/server/telecom/Analytics.java205
-rw-r--r--src/com/android/server/telecom/AnomalyReporterAdapter.java27
-rw-r--r--src/com/android/server/telecom/AnomalyReporterAdapterImpl.java27
-rw-r--r--src/com/android/server/telecom/AsyncRingtonePlayer.java143
-rw-r--r--src/com/android/server/telecom/Call.java630
-rw-r--r--src/com/android/server/telecom/CallAnomalyWatchdog.java440
-rw-r--r--src/com/android/server/telecom/CallAudioManager.java67
-rw-r--r--src/com/android/server/telecom/CallAudioModeStateMachine.java171
-rw-r--r--src/com/android/server/telecom/CallAudioRouteStateMachine.java146
-rw-r--r--src/com/android/server/telecom/CallDiagnosticServiceController.java15
-rw-r--r--src/com/android/server/telecom/CallEndpointController.java399
-rw-r--r--src/com/android/server/telecom/CallEndpointControllerFactory.java27
-rw-r--r--src/com/android/server/telecom/CallIntentProcessor.java3
-rw-r--r--[-rwxr-xr-x]src/com/android/server/telecom/CallLogManager.java7
-rw-r--r--src/com/android/server/telecom/CallScreeningServiceHelper.java5
-rw-r--r--src/com/android/server/telecom/CallState.java40
-rw-r--r--src/com/android/server/telecom/CallStreamingController.java443
-rw-r--r--[-rwxr-xr-x]src/com/android/server/telecom/CallsManager.java1294
-rw-r--r--src/com/android/server/telecom/CallsManagerListenerBase.java24
-rw-r--r--src/com/android/server/telecom/CarModeTracker.java4
-rw-r--r--src/com/android/server/telecom/ConnectionServiceFocusManager.java44
-rw-r--r--src/com/android/server/telecom/ConnectionServiceWrapper.java341
-rw-r--r--src/com/android/server/telecom/CreateConnectionProcessor.java50
-rw-r--r--src/com/android/server/telecom/DefaultDialerCache.java3
-rw-r--r--src/com/android/server/telecom/DockManager.java1
-rw-r--r--src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java438
-rw-r--r--src/com/android/server/telecom/EmergencyCallHelper.java137
-rw-r--r--src/com/android/server/telecom/HeadsetMediaButton.java8
-rwxr-xr-xsrc/com/android/server/telecom/InCallAdapter.java38
-rw-r--r--src/com/android/server/telecom/InCallController.java1029
-rw-r--r--src/com/android/server/telecom/InCallTonePlayer.java8
-rw-r--r--src/com/android/server/telecom/LogUtils.java22
-rw-r--r--src/com/android/server/telecom/MmiUtils.java184
-rw-r--r--src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java59
-rw-r--r--src/com/android/server/telecom/ParcelableCallUtils.java3
-rw-r--r--src/com/android/server/telecom/PhoneAccountRegistrar.java314
-rw-r--r--src/com/android/server/telecom/RespondViaSmsManager.java5
-rwxr-xr-xsrc/com/android/server/telecom/RespondViaSmsSettings.java2
-rw-r--r--src/com/android/server/telecom/Ringer.java528
-rw-r--r--src/com/android/server/telecom/RingerAttributes.java17
-rw-r--r--src/com/android/server/telecom/RingtoneFactory.java47
-rw-r--r--src/com/android/server/telecom/RoleManagerAdapter.java4
-rw-r--r--src/com/android/server/telecom/RoleManagerAdapterImpl.java21
-rw-r--r--src/com/android/server/telecom/StatusBarNotifier.java14
-rw-r--r--src/com/android/server/telecom/StreamingCallAdapter.java72
-rw-r--r--src/com/android/server/telecom/SystemStateHelper.java2
-rw-r--r--src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java36
-rw-r--r--src/com/android/server/telecom/TelecomServiceImpl.java720
-rw-r--r--src/com/android/server/telecom/TelecomSystem.java89
-rw-r--r--src/com/android/server/telecom/Timeouts.java236
-rw-r--r--src/com/android/server/telecom/TransactionalServiceRepository.java80
-rw-r--r--src/com/android/server/telecom/TransactionalServiceWrapper.java659
-rw-r--r--src/com/android/server/telecom/TtyManager.java1
-rw-r--r--src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java1
-rw-r--r--src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java29
-rw-r--r--src/com/android/server/telecom/callfiltering/CallFilteringResult.java28
-rw-r--r--src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java3
-rw-r--r--src/com/android/server/telecom/callfiltering/DndCallFilter.java72
-rw-r--r--src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java2
-rw-r--r--src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java59
-rw-r--r--src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java6
-rw-r--r--src/com/android/server/telecom/components/TelecomService.java42
-rw-r--r--src/com/android/server/telecom/components/UserCallActivity.java3
-rwxr-xr-xsrc/com/android/server/telecom/components/UserCallIntentProcessor.java70
-rw-r--r--src/com/android/server/telecom/settings/BlockedNumbersActivity.java10
-rw-r--r--src/com/android/server/telecom/stats/CallFailureCause.java69
-rw-r--r--src/com/android/server/telecom/stats/CallStateChangedAtomWriter.java119
-rw-r--r--src/com/android/server/telecom/ui/AudioProcessingNotification.java20
-rw-r--r--src/com/android/server/telecom/ui/CallStreamingNotification.java324
-rw-r--r--src/com/android/server/telecom/ui/DisconnectedCallNotifier.java3
-rw-r--r--src/com/android/server/telecom/ui/IncomingCallNotifier.java17
-rw-r--r--src/com/android/server/telecom/ui/NotificationChannelManager.java15
-rw-r--r--src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java144
-rw-r--r--src/com/android/server/telecom/voip/EndCallTransaction.java62
-rw-r--r--src/com/android/server/telecom/voip/EndpointChangeTransaction.java59
-rw-r--r--src/com/android/server/telecom/voip/HoldCallTransaction.java56
-rw-r--r--src/com/android/server/telecom/voip/IncomingCallTransaction.java89
-rw-r--r--src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java64
-rw-r--r--src/com/android/server/telecom/voip/OutgoingCallTransaction.java133
-rw-r--r--src/com/android/server/telecom/voip/ParallelTransaction.java109
-rw-r--r--src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java105
-rw-r--r--src/com/android/server/telecom/voip/SerialTransaction.java118
-rw-r--r--src/com/android/server/telecom/voip/TransactionManager.java130
-rw-r--r--src/com/android/server/telecom/voip/VoipCallMonitor.java365
-rw-r--r--src/com/android/server/telecom/voip/VoipCallTransaction.java110
-rw-r--r--src/com/android/server/telecom/voip/VoipCallTransactionResult.java77
-rw-r--r--testapps/AndroidManifest.xml9
-rw-r--r--testapps/res/layout/self_managed_sample_main.xml6
-rw-r--r--testapps/res/values/colors.xml1
-rw-r--r--testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java20
-rw-r--r--testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java28
-rw-r--r--testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java4
-rw-r--r--testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java11
-rw-r--r--testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java13
-rw-r--r--testapps/streamingtest/Android.bp31
-rw-r--r--testapps/streamingtest/AndroidManifest.xml38
-rw-r--r--testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java46
-rw-r--r--testapps/transactionalVoipApp/Android.bp28
-rw-r--r--testapps/transactionalVoipApp/AndroidManifest.xml62
-rw-r--r--testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.pngbin0 -> 341 bytes
-rw-r--r--testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.pngbin0 -> 213 bytes
-rw-r--r--testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.pngbin0 -> 343 bytes
-rw-r--r--testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.pngbin0 -> 519 bytes
-rw-r--r--testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.pngbin0 -> 641 bytes
-rw-r--r--testapps/transactionalVoipApp/res/layout/in_call_activity.xml110
-rw-r--r--testapps/transactionalVoipApp/res/layout/main_activity.xml68
-rw-r--r--testapps/transactionalVoipApp/res/raw/sample_audio.oggbin0 -> 18617 bytes
-rw-r--r--testapps/transactionalVoipApp/res/raw/sample_audio2.oggbin0 -> 11142 bytes
-rw-r--r--testapps/transactionalVoipApp/res/values-af/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-am/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ar/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-as/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-az/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-b+sr+Latn/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-be/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-bg/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-bn/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-bs/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ca/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-cs/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-da/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-de/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-el/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-en-rAU/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-en-rCA/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-en-rGB/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-en-rIN/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-en-rXC/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-es-rUS/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-es/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-et/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-eu/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-fa/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-fi/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-fr/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-gl/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-gu/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-hi/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-hr/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-hu/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-hy/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-in/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-is/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-it/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-iw/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ja/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ka/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-kk/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-km/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-kn/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ko/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ky/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-lo/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-lt/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-lv/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-mk/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ml/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-mn/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-mr/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ms/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-my/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-nb/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ne/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-nl/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-or/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-pa/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-pl/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-pt-rPT/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-pt/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ro/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ru/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-si/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-sk/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-sl/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-sq/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-sr/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-sv/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-sw/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ta/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-te/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-th/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-tl/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-tr/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-uk/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-ur/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-uz/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-vi/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-zh-rHK/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-zh-rTW/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values-zu/strings.xml37
-rw-r--r--testapps/transactionalVoipApp/res/values/strings.xml44
-rw-r--r--testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java76
-rw-r--r--testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java303
-rw-r--r--testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java123
-rw-r--r--testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java176
-rw-r--r--testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java146
-rw-r--r--tests/AndroidManifest.xml8
-rw-r--r--tests/src/com/android/server/telecom/tests/BasicCallTests.java176
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java867
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAttributesTests.java126
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java25
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java62
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java186
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java126
-rw-r--r--tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java8
-rw-r--r--tests/src/com/android/server/telecom/tests/CallControlTest.java78
-rw-r--r--tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java289
-rw-r--r--tests/src/com/android/server/telecom/tests/CallExceptionTests.java82
-rw-r--r--tests/src/com/android/server/telecom/tests/CallExtrasTest.java6
-rw-r--r--tests/src/com/android/server/telecom/tests/CallFailureCauseTest.java63
-rw-r--r--tests/src/com/android/server/telecom/tests/CallLogManagerTest.java2
-rw-r--r--tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java36
-rw-r--r--tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java15
-rw-r--r--tests/src/com/android/server/telecom/tests/CallTest.java628
-rw-r--r--tests/src/com/android/server/telecom/tests/CallsManagerTest.java1642
-rw-r--r--tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java22
-rw-r--r--tests/src/com/android/server/telecom/tests/ComponentContextFixture.java55
-rwxr-xr-xtests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java38
-rw-r--r--tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java145
-rw-r--r--tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java1
-rw-r--r--tests/src/com/android/server/telecom/tests/DndCallFilteringTests.java132
-rw-r--r--tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java305
-rw-r--r--tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java271
-rw-r--r--tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java113
-rw-r--r--tests/src/com/android/server/telecom/tests/InCallControllerTests.java710
-rw-r--r--tests/src/com/android/server/telecom/tests/InCallServiceFixture.java11
-rw-r--r--tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java13
-rw-r--r--tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java47
-rw-r--r--tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java93
-rw-r--r--tests/src/com/android/server/telecom/tests/MissedInformationTest.java8
-rw-r--r--tests/src/com/android/server/telecom/tests/MmiUtilsTest.java94
-rw-r--r--tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java66
-rw-r--r--tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java8
-rw-r--r--tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java521
-rw-r--r--tests/src/com/android/server/telecom/tests/RingerTest.java517
-rw-r--r--tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java985
-rw-r--r--tests/src/com/android/server/telecom/tests/TelecomSystemTest.java126
-rw-r--r--tests/src/com/android/server/telecom/tests/TestScheduledExecutorService.java283
-rw-r--r--tests/src/com/android/server/telecom/tests/TransactionTests.java283
-rw-r--r--tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java182
-rw-r--r--tests/src/com/android/server/telecom/tests/VideoCallTests.java16
-rw-r--r--tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java304
-rw-r--r--tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java253
335 files changed, 25135 insertions, 2188 deletions
diff --git a/Android.bp b/Android.bp
index 1b422aaf7..501b43863 100644
--- a/Android.bp
+++ b/Android.bp
@@ -27,6 +27,10 @@ android_app {
],
static_libs: [
"androidx.annotation_annotation",
+ "androidx.core_core",
+ ],
+ libs: [
+ "services",
],
resource_dirs: ["res"],
proto: {
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c26650ccc..ab067d918 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -63,7 +63,10 @@
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"/>
<uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
<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.STATUS_BAR_SERVICE" />
<permission android:name="android.permission.BROADCAST_CALLLOG_INFO"
android:label="Broadcast the call type/duration information"
@@ -137,6 +140,7 @@
android:permission="android.permission.CALL_PHONE"
android:excludeFromRecents="true"
android:process=":ui"
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|density|fontScale|keyboard|layoutDirection|locale|navigation|smallestScreenSize|touchscreen|uiMode"
android:exported="true">
<!-- CALL action intent filters for the various ways of initiating an outgoing call. -->
<intent-filter>
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 9874044e7..489fab779 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,2 +1,3 @@
-[Hook Scripts]
-aosp_hook = ${REPO_ROOT}/packages/services/Telecomm/scripts/aosp_tag_preupload.py ${PREUPLOAD_COMMIT}
+# Uncomment to re-enable aosp warning.
+#[Hook Scripts]
+#aosp_hook = ${REPO_ROOT}/packages/services/Telecomm/scripts/aosp_tag_preupload.py ${PREUPLOAD_COMMIT}
diff --git a/res/drawable/gm_phonelink.xml b/res/drawable/gm_phonelink.xml
new file mode 100644
index 000000000..2ffba0e42
--- /dev/null
+++ b/res/drawable/gm_phonelink.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M5,6h16L21,4L5,4c-1.1,0 -2,0.9 -2,2v11L1,17v3h11v-3L5,17L5,6zM21,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L22,9c0,-0.55 -0.45,-1 -1,-1zM20,17h-4v-7h4v7z"/>
+</vector>
diff --git a/res/drawable/person_circle.xml b/res/drawable/person_circle.xml
new file mode 100644
index 000000000..e139b4f59
--- /dev/null
+++ b/res/drawable/person_circle.xml
@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M21.094,0.92C22.839,-0.307 25.161,-0.307 26.906,0.92C28.031,1.71 29.428,2.009 30.777,1.746C32.868,1.339 34.989,2.287 36.086,4.12C36.794,5.302 37.95,6.145 39.288,6.456C41.363,6.938 42.917,8.671 43.177,10.794C43.345,12.163 44.059,13.406 45.156,14.236C46.856,15.524 47.574,17.742 46.952,19.788C46.551,21.107 46.7,22.534 47.365,23.741C48.397,25.612 48.154,27.931 46.758,29.546C45.857,30.587 45.416,31.951 45.535,33.326C45.72,35.457 44.559,37.476 42.629,38.381C41.384,38.965 40.429,40.03 39.981,41.335C39.287,43.357 37.408,44.727 35.279,44.766C33.906,44.79 32.601,45.374 31.664,46.382C30.211,47.946 27.939,48.431 25.979,47.596C24.714,47.057 23.286,47.057 22.021,47.596C20.061,48.431 17.789,47.946 16.336,46.382C15.399,45.374 14.094,44.79 12.721,44.766C10.592,44.727 8.713,43.357 8.019,41.335C7.571,40.03 6.616,38.965 5.371,38.381C3.441,37.476 2.28,35.457 2.465,33.326C2.584,31.951 2.143,30.587 1.242,29.546C-0.154,27.931 -0.397,25.612 0.635,23.741C1.3,22.534 1.449,21.107 1.048,19.788C0.427,17.742 1.144,15.524 2.844,14.236C3.941,13.406 4.655,12.163 4.823,10.794C5.083,8.671 6.637,6.938 8.712,6.456C10.05,6.145 11.206,5.302 11.914,4.12C13.011,2.287 15.132,1.339 17.223,1.746C18.572,2.009 19.969,1.71 21.094,0.92Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M24.001,15.467C21.644,15.467 19.734,17.376 19.734,19.733C19.734,22.091 21.644,24 24.001,24C26.358,24 28.268,22.091 28.268,19.733C28.268,17.376 26.358,15.467 24.001,15.467ZM26.134,19.734C26.134,18.56 25.174,17.601 24,17.601C22.827,17.601 21.867,18.56 21.867,19.734C21.867,20.907 22.827,21.867 24,21.867C25.174,21.867 26.134,20.907 26.134,19.734ZM30.402,29.333C30.188,28.576 26.882,27.2 24.002,27.2C21.122,27.2 17.815,28.576 17.602,29.344V30.4H30.402V29.333ZM15.469,29.333C15.469,26.496 21.154,25.066 24.002,25.066C26.85,25.066 32.535,26.496 32.535,29.333V32.533H15.469V29.333Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index a4b41db82..50bead54c 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Agtergrondoproepe"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Oproepe is ontkoppel"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Omgevalde foonprogramme"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Oproepstroming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"As jy hierdie oproep maak, sal dit jou <xliff:g id="OTHER_APP">%1$s</xliff:g>-oproep beëindig."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Kies hoe om hierdie oproep te maak"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Herlei oproep deur <xliff:g id="OTHER_APP">%1$s</xliff:g> te gebruik"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom-ontwikkelaarkieslys"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Oproepe kan nie gedurende \'n noodoproep geneem word nie."</string>
<string name="cancel" msgid="6733466216239934756">"Kanselleer"</string>
+ <string name="back" msgid="6915955601805550206">"Terug"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Oorstuk"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Kabelkopstuk"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Luidspreker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ekstern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Onbekend"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Stroom oudio na ander toestel"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Beëindig oproep"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Skakel hier oor"</string>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 3e91608c8..f0923d5ca 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"የጀርባ ጥሪዎች"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"የተቋረጡ ጥሪዎች"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"የተበላሹ የስልክ መተግበሪያዎች"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"የጥሪ ዥረት"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ይህን ጥሪ ማድረግ የ<xliff:g id="OTHER_APP">%1$s</xliff:g> ጥሪዎን ያቋርጣል።"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ይህን ጥሪ እንዴት እንደሚያደርጉ ይምረጡ"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g>ን በመጠቀም አዘዋውር"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"የቴሌኮም ገንቢ ምናሌ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ጥሪዎች በአደጋ ጊዜ ጥሪ ላይ ሊነሱ አይችሉም።"</string>
<string name="cancel" msgid="6733466216239934756">"ይቅር"</string>
+ <string name="back" msgid="6915955601805550206">"ተመለስ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ማዳመጫ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ብሉቱዝ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ባለገመድ ማዳመጫ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ድምፅ ማውጫ"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ውጫዊ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ያልታወቀ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ኦዲዮን ወደ ሌላ መሣሪያ በመልቀቅ ላይ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ዝጋ"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"እዚህ ቀይር"</string>
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index fe3960328..2a568091b 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"مكالمات في الخلفية"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"المكالمات التي تم قطع الاتصال بها"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"تطبيقات الهواتف المعطّلة"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"بث المكالمات"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"يؤدي إجراء هذه المكالمة إلى إنهاء مكالمة <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"اختيار كيفية إجراء هذه المكالمة"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"إعادة توجيه المكالمة باستخدام <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"قائمة مطوّر برامج الاتصالات"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"لا يمكن تلقّي المكالمات أثناء إجراء مكالمة طوارئ."</string>
<string name="cancel" msgid="6733466216239934756">"إلغاء"</string>
+ <string name="back" msgid="6915955601805550206">"رجوع"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"سماعة الأذن"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"البلوتوث"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"سماعة رأس سلكية"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"مكبّر صوت"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"المصادر الخارجية"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"غير معروف"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"بث الصوت على جهاز آخر"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"قطع الاتصال"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"الانتقال إلى هنا"</string>
</resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 004e62e15..72ac4dbb9 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"নেপথ্যৰ কলসমূহ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"সংযোগ বিচ্ছিন্ন কৰা কলসমূহ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ক্ৰেশ্ব হোৱা ফ\'ন এপ্‌সমূহ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"কল ষ্ট্ৰীমিং"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"এই কলটো কৰিলে আপোনাৰ <xliff:g id="OTHER_APP">%1$s</xliff:g> কলটোৰ অন্ত পৰিব।"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"এই কলটো কেনেকৈ কৰা হ’ব সেয়া বাছনি কৰক"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ব্যৱহাৰ কৰি কল ৰিডাইৰেক্ট কৰক"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"দূৰ-সংযোগ সম্পৰ্কীয় বিকাশকৰ্তাৰ মেনু"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"কোনো জৰুৰীকালীন কলত থাকিলে কলসমূহ গ্ৰহণ কৰিব নোৱাৰি।"</string>
<string name="cancel" msgid="6733466216239934756">"বাতিল কৰক"</string>
+ <string name="back" msgid="6915955601805550206">"উভতি যাওক"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ইয়েৰপিচ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ব্লুটুথ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"তাঁৰযুক্ত হেডছেট"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"স্পীকাৰ"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"বাহ্যিক"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"অজ্ঞাত"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"অন্য এটা ডিভাইচলৈ অডিঅ’ ষ্ট্ৰীম কৰি থকা হৈছে"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"কলটো কাটি দিয়ক"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ইয়াত সলনি কৰক"</string>
</resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index c8e403b60..ead7f5433 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Arxa fon zəngləri"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Kəsilmiş zənglər"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Tətbiq xətaları"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Zəng yayımı"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Bu zəngin yerləşdirilməsi <xliff:g id="OTHER_APP">%1$s</xliff:g> zəngini sonlandıracaq."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Bu zəngi necə etməyi seçin"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> istifadə edərək zəngi yönləndirin"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom Tərtibatçı Menyusu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Təcili zəng zamanı zəng edilə bilməz."</string>
<string name="cancel" msgid="6733466216239934756">"Ləğv edin"</string>
+ <string name="back" msgid="6915955601805550206">"Geri"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Qulaqlıq"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Simli qulaqlıq"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Dinamik"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Xarici"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Naməlum"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio digər cihaza ötürülür"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Zəngi sonlandırın"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Buraya keçin"</string>
</resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 1a8b1fbb1..d5278427b 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -100,6 +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">"Aplikacije za telefoniranje koje su otkazale"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Strimovanje poziva"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ako uputite ovaj poziv, završićete <xliff:g id="OTHER_APP">%1$s</xliff:g> poziv."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Izaberite kako želite da uputite ovaj poziv"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Preusmeri poziv pomoću: <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Meni za programere Telecom-a"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Za vreme hitnog poziva nije moguće preuzimati druge pozive."</string>
<string name="cancel" msgid="6733466216239934756">"Otkaži"</string>
+ <string name="back" msgid="6915955601805550206">"Nazad"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Slušalica"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Žičane slušalice"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Zvučnik"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Eksterni"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Nepoznato"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Zvuk se strimuje na drugi uređaj"</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 ovde"</string>
</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 8fc4be72a..c5b59bddd 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Фонавыя выклікі"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Перарваныя выклікі"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Збоі ў праграмах \"Тэлефон\""</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Перадача выкліку плынню"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Калі зрабіць гэты выклік, ваш выклік праз праграму <xliff:g id="OTHER_APP">%1$s</xliff:g> скончыцца."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Выберыце, праз які нумар зрабіць выклік"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Перанакіраваць выклік, выкарыстоўваючы нумар \"<xliff:g id="OTHER_APP">%1$s</xliff:g>\""</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Меню распрацоўшчыка Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Падчас экстраннага выкліку іншыя выклікі прымаць немагчыма."</string>
<string name="cancel" msgid="6733466216239934756">"Скасаваць"</string>
+ <string name="back" msgid="6915955601805550206">"Назад"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Дынамік"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Правадная гарнітура"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Знешні дынамік"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Знешняя прылада"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Невядома"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Перадача аўдыя плынню на іншую прыладу"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Завяршыць выклік"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Пераключыцца"</string>
</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index f67820dc7..fe5d70f56 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Обаждания на заден план"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Прекъснати обаждания"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Приложения за телефон с прекъсната работа"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Поточно предаване на обаждания"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ако извършите това обаждане, обаждането ви през <xliff:g id="OTHER_APP">%1$s</xliff:g> ще прекъсне."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Изберете как да се извърши обаждането"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Пренасочване на обаждането през <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Меню за програмисти на Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"По време на спешно обаждане не могат да се поемат обаждания."</string>
<string name="cancel" msgid="6733466216239934756">"Отказ"</string>
+ <string name="back" msgid="6915955601805550206">"Назад"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Слушалка"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Слушалки с кабел"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Високоговорител"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Външно"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Неизвестно"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Звукът се предава поточно към друго устройство"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Затваряне"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Превключете тук"</string>
</resources>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 800db09bf..49e6ba320 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ব্যাকগ্রাউন্ডের কল"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ডিসকানেক্ট করা কলগুলি"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ক্র্যাশ হওয়া ফোন অ্যাপ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"কল স্ট্রিম করা হচ্ছে"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"এই কলটির উত্তর দেওয়া হলে তা আপনার <xliff:g id="OTHER_APP">%1$s</xliff:g> কলটি কেটে যাবে৷"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"এই কলটি কীভাবে করবেন বেছে নিন"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ব্যবহার করে কল রিডাইরেক্ট করুন"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"টেলিকম ডেভেলপার মেনু"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"জরুরি কল চলাকালীন কোনও কল রিসিভ করা যাবে না।"</string>
<string name="cancel" msgid="6733466216239934756">"বাতিল করুন"</string>
+ <string name="back" msgid="6915955601805550206">"ফিরে যান"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ইয়ারপিস"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ব্লুটুথ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ওয়্যার্ড হেডসেট"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"স্পিকার"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"এক্সটার্নাল"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"অজানা"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"অন্য ডিভাইসে অডিও স্ট্রিম করা হচ্ছে"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"কল কেটে দিন"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"এখানে পাল্টান"</string>
</resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index d8708d140..61b86db16 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -100,6 +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="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>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Meni za programere iz telekoma"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Pozivi se ne mogu primati tokom hitnog poziva"</string>
<string name="cancel" msgid="6733466216239934756">"Otkaži"</string>
+ <string name="back" msgid="6915955601805550206">"Nazad"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Slušalica"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Žičane slušalice"</string>
+ <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_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 45f182cd7..113d14428 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Trucades en segon pla"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Trucades desconnectades"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplicacions del telèfon que han fallat"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Reproducció en directe de trucada"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"En fer aquesta trucada, finalitzarà la de l\'aplicació <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Tria com vols fer aquesta trucada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Desvia la trucada amb <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menú per a desenvolupadors de telecomunicacions"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"No es poden respondre trucades durant una trucada d\'emergència."</string>
<string name="cancel" msgid="6733466216239934756">"Cancel·la"</string>
+ <string name="back" msgid="6915955601805550206">"Enrere"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auricular"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Auriculars amb cable"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Altaveu"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Extern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Desconegut"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"S\'està reproduint àudio en continu en un altre dispositiu"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Penja"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Canvia aquí"</string>
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 92384219e..ab74d613a 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Hovory na pozadí"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Odpojené hovory"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplikace, které spadly"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streamování hovoru"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Uskutečněním tohoto hovoru ukončíte hovor <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Vyberte, jak chcete tento hovor provést"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Přesměrovat hovor přes aplikaci <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Nabídka pro vývojáře Telecomu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Během tísňového volání není možné přijímat hovory."</string>
<string name="cancel" msgid="6733466216239934756">"Zrušit"</string>
+ <string name="back" msgid="6915955601805550206">"Zpět"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Sluchátko"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Kabelová náhlavní souprava"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Reproduktor"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externí"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Není známo"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streamování zvuku do druhého zařízení"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Zavěsit"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Přepnout sem"</string>
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 9e755ad32..fff725758 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Opkald i baggrunden"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Afbrudte opkald"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Opkaldsapps, der er gået ned"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Opkaldsstreaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Hvis du foretager dette opkald, afsluttes dit opkald i <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Vælg, hvordan du vil foretage dette opkald"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Omdiriger opkaldet ved hjælp af <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Udviklermenu for Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Du kan ikke besvare opkald, mens du er i et nødopkald."</string>
<string name="cancel" msgid="6733466216239934756">"Annuller"</string>
+ <string name="back" msgid="6915955601805550206">"Tilbage"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Højttaler"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Headset med ledning"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Højttaler"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ekstern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Ukendt"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streamer lyd til en anden enhed"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Læg på"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Skift hertil"</string>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 659ff9103..dccdb87c0 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Anrufe im Hintergrund"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Beendete Anrufe"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Abgestürzte Telefon-Apps"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Anrufstreaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Durch diesen Anruf wird der Anruf in <xliff:g id="OTHER_APP">%1$s</xliff:g> beendet."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Wie möchtest du anrufen?"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Mit <xliff:g id="OTHER_APP">%1$s</xliff:g> weiterleiten"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom-Entwicklermenü"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Während eines Notrufs kannst du keine Anrufe annehmen."</string>
<string name="cancel" msgid="6733466216239934756">"Abbrechen"</string>
+ <string name="back" msgid="6915955601805550206">"Zurück"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Kopfhörer"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Kabelgebundenes Headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Lautsprecher"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Extern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Unbekannt"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio auf einem anderen Gerät streamen"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Anruf beenden"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Auf dieses Gerät wechseln"</string>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 91d94d494..2cf961d4d 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Κλήσεις στο παρασκήνιο"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Αποσυνδεδεμένες κλήσεις"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Εφαρμογές τηλεφώνου που αντιμετώπισαν σφάλμα λειτουργίας"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Ροή κλήσης"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Εάν πραγματοποιήσετε αυτήν την κλήση, η κλήση σας μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g> θα τερματιστεί."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Επιλέξτε πώς θα πραγματοποιήσετε την κλήση"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Ανακατεύθυνση της κλήσης μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Μενού προγραμματιστών τηλεπικοινωνιών"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Δεν είναι δυνατή η λήψη κλήσεων κατά τη διάρκεια κλήσης επείγουσας ανάγκης."</string>
<string name="cancel" msgid="6733466216239934756">"Ακύρωση"</string>
+ <string name="back" msgid="6915955601805550206">"Πίσω"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Ακουστικό"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Ενσύρματα ακουστικά"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Ηχείο"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Εξωτερικά"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Άγνωστο"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Ροή ήχου σε άλλη συσκευή"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Απόρριψη"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Εναλλαγή εδώ"</string>
</resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 52a1c64d5..250ab62ed 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Background calls"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Disconnected calls"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Crashed phone apps"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Call streaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Placing this call will end your <xliff:g id="OTHER_APP">%1$s</xliff:g> call."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choose how to make this call"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirect call using <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom Developer Menu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Calls can not be taken while in an emergency call."</string>
<string name="cancel" msgid="6733466216239934756">"Cancel"</string>
+ <string name="back" msgid="6915955601805550206">"Back"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Earpiece"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"External"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Unknown"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
</resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 55df4743d..e6291f47b 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Background calls"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Disconnected calls"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Crashed phone apps"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Call streaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Placing this call will end your <xliff:g id="OTHER_APP">%1$s</xliff:g> call."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choose how to place this call"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirect call using <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom Developer Menu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Calls can not be taken while in an emergency call."</string>
<string name="cancel" msgid="6733466216239934756">"Cancel"</string>
+ <string name="back" msgid="6915955601805550206">"Back"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Earpiece"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"External"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Unknown"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 52a1c64d5..250ab62ed 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Background calls"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Disconnected calls"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Crashed phone apps"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Call streaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Placing this call will end your <xliff:g id="OTHER_APP">%1$s</xliff:g> call."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choose how to make this call"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirect call using <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom Developer Menu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Calls can not be taken while in an emergency call."</string>
<string name="cancel" msgid="6733466216239934756">"Cancel"</string>
+ <string name="back" msgid="6915955601805550206">"Back"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Earpiece"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"External"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Unknown"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
</resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 52a1c64d5..250ab62ed 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Background calls"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Disconnected calls"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Crashed phone apps"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Call streaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Placing this call will end your <xliff:g id="OTHER_APP">%1$s</xliff:g> call."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choose how to make this call"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirect call using <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom Developer Menu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Calls can not be taken while in an emergency call."</string>
<string name="cancel" msgid="6733466216239934756">"Cancel"</string>
+ <string name="back" msgid="6915955601805550206">"Back"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Earpiece"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Wired headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"External"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Unknown"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
</resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index e5d1332a1..5bd0e25eb 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‏‏‎‎‎‎‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‏‎‎Background calls‎‏‎‎‏‎"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‎‎‎‏‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‎‏‏‏‎‏‏‏‎‏‏‎‎‎‏‏‏‎‎‎‏‎‎‏‏‏‎‏‏‏‏‎‏‎Disconnected calls‎‏‎‎‏‎"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎‏‏‎‎‎‏‏‎‏‏‎‎‏‎‎‎‎‎‎‎‎‎‎‏‏‏‎‏‎‎‎‎‎‏‎‎‏‎‏‏‎Crashed phone apps‎‏‎‎‏‎"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‎‏‎‎‎‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎‎‎‏‏‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎Call streaming‎‏‎‎‏‎"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‏‎‏‎‎‎‎‎‏‎‎‎‎‎‏‏‎‏‎‏‎‎‏‏‏‏‎‎‎‎‎‎‏‏‎‎‏‏‎‏‏‎‏‏‏‎‏‎‎‎‏‏‏‎Placing this call will end your ‎‏‎‎‏‏‎<xliff:g id="OTHER_APP">%1$s</xliff:g>‎‏‎‎‏‏‏‎ call.‎‏‎‎‏‎"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‎‏‎‎‏‎‎‏‏‏‏‎‎‎‎‎‎‎‎‏‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‏‎‎‏‎‎‏‏‎‏‏‏‎‎‏‎‎‎Choose how to place this call‎‏‎‎‏‎"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‎‎‎‎‏‏‏‎‏‎‎‏‏‏‏‎‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‏‏‎‎‎‎‏‏‎‏‏‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎Redirect call using ‎‏‎‎‏‏‎<xliff:g id="OTHER_APP">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‏‏‎‏‏‎‎‏‎‏‏‎‏‏‏‎‏‏‏‏‏‎‏‏‏‎‏‏‏‏‎‏‎‎‎‎‏‎Telecom Developer Menu‎‏‎‎‏‎"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‎‏‏‏‏‏‎‎‎‏‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‏‎‏‎‏‎Calls can not be taken while in an emergency call.‎‏‎‎‏‎"</string>
<string name="cancel" msgid="6733466216239934756">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‏‏‏‎‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‎‏‎‎‏‏‎‎‏‎‏‏‎‏‎‎‏‎‎‏‎‎‎Cancel‎‏‎‎‏‎"</string>
+ <string name="back" msgid="6915955601805550206">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‎‎‎‏‏‏‏‏‎‏‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‏‎‎‏‏‏‏‏‏‎‎Back‎‏‎‎‏‎"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‎‏‏‎‎‏‏‏‏‏‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‎‏‏‏‎‏‎‏‎‏‏‎‎‎‏‎‎‎‎‎‏‎‎Earpiece‎‏‎‎‏‎"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‎‏‎‏‎‏‏‏‎‏‎‏‎‏‏‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‎‎‏‎‎‏‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‎‏‎‏‎‎‎Bluetooth‎‏‎‎‏‎"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‎‏‏‎‎‏‏‎‏‎‎‏‏‎‎‏‎‏‏‎‎‏‎‎‎‎‎‏‏‎‎‏‏‎‎‎‏‏‏‎‎‏‏‎‎‏‏‏‏‏‏‎‎Wired headset‎‏‎‎‏‎"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‎‏‎‏‏‏‎‏‎‎‎‏‏‎‎‏‏‎‏‏‎‎‏‏‎‎‎‎‏‏‏‎‏‎‏‏‏‏‏‎‎‎‎‏‎‎‏‎‎‎‏‏‎‏‎‏‎Speaker‎‏‎‎‏‎"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‎‎‎‎‎‎‏‏‏‎‎‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‏‎‎‏‎‎‎‏‎‎‏‏‏‏‎‎‎‏‏‏‎‎‏‏‏‏‎‎‎‎External‎‏‎‎‏‎"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‏‏‎‏‎‎‎‎‏‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎‎‎‏‎‏‎‏‎‏‎‎‎‎‏‎‏‏‏‎‎‏‏‏‏‏‎‎‎Unknown‎‏‎‎‏‎"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‎‏‏‏‏‎‏‏‏‏‏‎‎‎‎‎‏‏‏‎‏‎‏‏‏‎‎‎‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‎‎‎‎‎‏‎‏‎‏‏‏‎Streaming audio to other device‎‏‎‎‏‎"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‎‎‎‏‏‏‎‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‏‎‎‏‏‎‏‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‏‎‎‏‎‎‏‏‎Hang up‎‏‎‎‏‎"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‏‏‏‎‏‎‎‎‎‏‏‎‎‏‏‏‏‎‏‏‏‎‎‏‎‏‎‏‏‎‎‏‏‎‎‎‏‎‎‏‏‎‏‏‎‏‏‎‏‎‏‎‏‏‎‎‎Switch here‎‏‎‎‏‎"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 4c6fe21b9..c0f4e179a 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Llamadas en segundo plano"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Llamadas desconectadas"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Apps de teléfono con fallas"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Transmisión de llamadas"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Si realizas esta llamada, finalizará la de <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Elige cómo quieres realizar esta llamada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redireccionar la llamada mediante <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menú para desarrolladores de Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"No puedes contestar llamadas mientras estés en una llamada de emergencia."</string>
<string name="cancel" msgid="6733466216239934756">"Cancelar"</string>
+ <string name="back" msgid="6915955601805550206">"Atrás"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auricular"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Auriculares con cable"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Bocina"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externa"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Desconocido"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Transmitiendo el audio a otro dispositivo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Colgar"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Cambiar aquí"</string>
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 1e5866af2..20b80a5e1 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Llamadas en segundo plano"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Llamadas interrumpidas"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplicaciones para teléfonos con fallos"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Transmisión de llamadas"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Si haces esta llamada, se finalizará la de <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Elige cómo quieres hacer esta llamada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirigir llamada con <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menú para desarrolladores de telecomunicaciones"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"No se pueden responder llamadas durante una llamada de emergencia."</string>
<string name="cancel" msgid="6733466216239934756">"Cancelar"</string>
+ <string name="back" msgid="6915955601805550206">"Atrás"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auricular"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Auriculares con cable"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Altavoz"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Fuentes externas"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Desconocido"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Transmitiendo audio a otro dispositivo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Colgar"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Cambiar aquí"</string>
</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 46d36bc7b..cac1fd6d3 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Taustal olevad kõned"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Katkestatud kõned"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Kokkujooksnud telefonirakendused"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Kõne voogesitus"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Selle kõne tegemisel lõpetatakse pooleliolev kõne rakenduses <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Valige, kuidas soovite helistada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Suuna kõne ümber rakenduse <xliff:g id="OTHER_APP">%1$s</xliff:g> abil"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Teenuse Telecom arendaja menüü"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Hädaabikõne ajal ei saa kõnesid vastu võtta."</string>
<string name="cancel" msgid="6733466216239934756">"Tühista"</string>
+ <string name="back" msgid="6915955601805550206">"Tagasi"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Kuular"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Juhtmega peakomplekt"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Kõlar"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Välised"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Teadmata"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Heli voogesitamine teise seadmesse"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Lõpeta kõne"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Vaheta siia"</string>
</resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index d6abf2149..d1aa5457e 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Atzeko planoko deiak"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Deskonektatutako deiak"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Huts egin duten telefonoko aplikazioak"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Deiak igortzea"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Dei hau egiten baduzu, amaitu egingo da <xliff:g id="OTHER_APP">%1$s</xliff:g> aplikazioko deia."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Aukeratu dei hau egiteko modua"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Birbideratu deia <xliff:g id="OTHER_APP">%1$s</xliff:g> aplikazioaren bidez"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telekomunikazioen garatzaileen menua"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Ezin duzu hartu deirik larrialdi-dei bat abian den bitartean."</string>
<string name="cancel" msgid="6733466216239934756">"Utzi"</string>
+ <string name="back" msgid="6915955601805550206">"Atzera"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Aurikularrak"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetootha"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Entzungailu kableduna"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Bozgorailua"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Kanpokoa"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Ezezaguna"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Audioa beste gailu batera igortzen ari da"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Amaitu deia"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Aldatu hona"</string>
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 11f5491bb..8d562ec30 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"تماس‌های پس‌زمینه"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"تماس‌های قطع‌شده"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"برنامه‌های تلفن خراب"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"جاری‌سازی تماس"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"اگر این تماس را برقرار کنید، تماس <xliff:g id="OTHER_APP">%1$s</xliff:g> شما قطع می‌شود."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"انتخاب نحوه برقراری این تماس"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"هدایت تماس با استفاده از <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"‏منوی برنامه‌نویس Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"درحین برقراری تماسی اضطراری، نمی‌توان به تماس‌ها پاسخ داد."</string>
<string name="cancel" msgid="6733466216239934756">"لغو"</string>
+ <string name="back" msgid="6915955601805550206">"برگشتن"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"گوشی"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"بلوتوث"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"هدست سیمی"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"بلندگو"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"خارجی"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"نامشخص"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"درحال جاری‌سازی صدا به دستگاه دیگر"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"قطع تماس"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"انتقال در اینجا انجام شود"</string>
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 30809edf3..338e42921 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Taustapuhelut"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Katkaistut puhelut"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Kaatuneet puhelinsovellukset"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Puhelunstriimaus"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Tämän puhelun soittaminen päättää puhelun sovelluksessa <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Valitse, miten puhelu soitetaan"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Uudelleenohjaa puhelu sovelluksella <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Televiestinnän kehittäjävalikko"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Puheluita ei voi välittää hätäpuhelun aikana."</string>
<string name="cancel" msgid="6733466216239934756">"Peru"</string>
+ <string name="back" msgid="6915955601805550206">"Takaisin"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Kuuloke"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Langallinen kuulokemikrofoni"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Kaiutin"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ulkoinen"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Tuntematon"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Audiota striimataan toiselle laitteelle"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Lopeta puhelu"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Vaihda puhelimeen"</string>
</resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 789909ab4..aaf651f9c 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Appels en arrière-plan"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Appels déconnectés"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applications téléphoniques qui ont planté"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Diffusion en continu d\'appels"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Si vous passez cet appel, vous mettrez fin à l\'appel <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choisissez comment passer cet appel"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Rediriger l\'appel en utilisant <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu Telecom Developer"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Les appels ne peuvent pas être pris pendant un appel d\'urgence."</string>
<string name="cancel" msgid="6733466216239934756">"Annuler"</string>
+ <string name="back" msgid="6915955601805550206">"Retour"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Écouteur"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Casque d\'écoute filaire"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Haut-parleur"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externe"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Inconnu"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Diffusion audio en continu vers un autre appareil en cours…"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Raccrocher"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Revenir à cet appareil"</string>
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 75882d1c1..a14cbb13c 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Appels en arrière-plan"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Appels interrompus"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applications téléphoniques ayant planté"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming de l\'appel"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Si vous passez cet appel, vous mettrez fin à celui qui est en cours dans l\'application <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choisissez comment passer cet appel"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Rediriger l\'appel avec <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu Telecom Developer"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Impossible de prendre un appel au cours d\'un appel d\'urgence."</string>
<string name="cancel" msgid="6733466216239934756">"Annuler"</string>
+ <string name="back" msgid="6915955601805550206">"Retour"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Écouteur"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Casque filaire"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Haut-parleur"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externe"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Inconnu"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming de l\'audio sur un autre appareil"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Raccrocher"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Passer ici"</string>
</resources>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 23fa03633..8e82fcec2 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Chamadas en segundo plano"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Chamadas desconectadas"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Fallaron as aplicacións de teléfono"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Propagación de chamada"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ao facer esta chamada, finalizarase o túa chamada de <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Escolle como facer esta chamada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirixir a chamada con <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menú para programadores de telecomunicacións"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Non se pode responder ás chamadas durante unha chamada de emerxencia."</string>
<string name="cancel" msgid="6733466216239934756">"Cancelar"</string>
+ <string name="back" msgid="6915955601805550206">"Volver"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auricular"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Auriculares con cable"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Altofalante"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externo"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Descoñecido"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Emitindo audio noutro dispositivo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Colgar"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Volver aquí"</string>
</resources>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 2b2ad64f3..1b5c5ce5a 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"બૅકગ્રાઉન્ડ કૉલ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ડિસ્કનેક્ટ કરેલા કૉલ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ફોન ઍપ ક્રૅશ થઈ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"કૉલ સ્ટ્રીમ કરી રહ્યાં છીએ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"આ કૉલ કરવાથી તમારો <xliff:g id="OTHER_APP">%1$s</xliff:g> કૉલ સમાપ્ત થઈ જશે."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"આ કૉલ કેવી રીતે કરવો તે પસંદ કરો"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g>નો ઉપયોગ કરીને કૉલ રીડાયરેક્ટ કરો"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ટેલિકોમ ડેવલપર મેનૂ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ઇમર્જન્સી કૉલ ચાલુ હોય, ત્યારે બીજા કોઈ કૉલ લઈ શકાતા નથી."</string>
<string name="cancel" msgid="6733466216239934756">"રદ કરો"</string>
+ <string name="back" msgid="6915955601805550206">"પાછળ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ઇયરપીસ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"બ્લૂટૂથ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"વાયરવાળું હૅડસેટ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"સ્પીકર"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"બાહ્ય"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"અજાણ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ઑડિયોને અન્ય ડિવાઇસ પર સ્ટ્રીમ કરી રહ્યાં છીએ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"સમાપ્ત કરો"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"અહીં સ્વિચ કરો"</string>
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index e9096867d..c32f58268 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"बैकग्राउंड कॉल"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"डिसकनेक्ट किए गए कॉल"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"फ़ोन ऐप्लिकेशन जो बंद हो गए"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"कॉल स्ट्रीमिंग"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"इस कॉल को करने से आपका <xliff:g id="OTHER_APP">%1$s</xliff:g> कॉल खत्म हो जाएगा."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"चुनें कि आप इस कॉल को कैसे करना चाहते हैं"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> का इस्तेमाल करके कॉल को दूसरे नंबर पर भेजें"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"टेलीकॉम डेवलपर मेन्यू"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"आपातकालीन कॉल के दौरान कॉल नहीं उठाया जा सकता."</string>
<string name="cancel" msgid="6733466216239934756">"रद्द करें"</string>
+ <string name="back" msgid="6915955601805550206">"वापस जाएं"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ईयरपीस"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ब्लूटूथ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"वायर वाला हेडसेट"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"स्पीकर"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"बाहरी सोर्स"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"कोई जानकारी नहीं है"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ऑडियो को दूसरे डिवाइस पर स्ट्रीम किया जा रहा है"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"कॉल खत्म करें"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"यहां स्विच करें"</string>
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 7adae6140..d6b209e08 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -100,6 +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">"Rušenja aplikacija telefona"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming poziva"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Upućivanjem ovog poziva prekinut ćete poziv u aplikaciji <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Odaberite kako ćete uputiti poziv"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Preusmjeri poziv putem aplikacije <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Izbornik Telecom Developer"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Pozivi se ne mogu primiti tijekom hitnog poziva."</string>
<string name="cancel" msgid="6733466216239934756">"Odustani"</string>
+ <string name="back" msgid="6915955601805550206">"Natrag"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Slušalica"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Žičane slušalice"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Zvučnik"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Vanjski izvori"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Nepoznato"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming zvuka na drugi uređaj"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini vezu"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Promijeni ovdje"</string>
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index b9775e967..63f04b653 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Háttérbeli hívások"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Bontott hívások"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Telefonalkalmazások összeomlása"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Hívás átvitele"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ha hívást indít, azzal megszakítja a(z) <xliff:g id="OTHER_APP">%1$s</xliff:g>-hívást."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"A hívás módjának kiválasztása"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Hívás átirányítása a következővel: <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telekommunikációs fejlesztői menü"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Segélyhívás közben nem lehet hívást fogadni."</string>
<string name="cancel" msgid="6733466216239934756">"Mégse"</string>
+ <string name="back" msgid="6915955601805550206">"Vissza"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Fülhallgató"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Vezetékes headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Hangszóró"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Külső"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Ismeretlen"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Hang átvitele másik eszközre"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hívás befejezése"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Váltás itt"</string>
</resources>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 962fd893e..169ea3602 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Ֆոնային զանգեր"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Անջատված զանգեր"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Հեռախոսի հավելվածներ, որոնց աշխատանքը սխալի պատճառով խափանվել է"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Զանգի հեռարձակում"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Այս զանգը կատարելու դեպքում <xliff:g id="OTHER_APP">%1$s</xliff:g>-ի ընթացիկ զանգը կընդհատվի"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Ընտրեք, թե ինչպես եք ուզում կատարել այս զանգը"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Վերահասցեավորել զանգը <xliff:g id="OTHER_APP">%1$s</xliff:g> հավելվածով"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom-ի մշակողի ընտրացանկ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Շտապ կանչի ժամանակ այլ զանգեր չեք կարող ընդւնել։"</string>
<string name="cancel" msgid="6733466216239934756">"Չեղարկել"</string>
+ <string name="back" msgid="6915955601805550206">"Հետ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Լսափող"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Լարով ականջակալ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Բարձրախոս"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Արտաքին"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Անհայտ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Աուդիոյի հեռարձակում այլ սարքում"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Ավարտել զանգը"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Անցնել այստեղ"</string>
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index c378d37fb..1e51f7a66 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Panggilan telepon latar belakang"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Panggilan terputus"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplikasi telepon error"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming panggilan"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Melakukan panggilan ini akan mengakhiri panggilan <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Pilih cara melakukan panggilan ini"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Alihkan panggilan menggunakan <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu Developer Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Panggilan tidak dapat diterima saat sedang melakukan panggilan darurat."</string>
<string name="cancel" msgid="6733466216239934756">"Batal"</string>
+ <string name="back" msgid="6915955601805550206">"Kembali"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Earpiece"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Headset berkabel"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Eksternal"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Tidak diketahui"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio ke perangkat lain"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Akhiri"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Beralih ke sini"</string>
</resources>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 7d3b6bb78..7009b7cb9 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Bakgrunnssímtöl"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Aftengd símtöl"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Hrun í símaforritum"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Símtal í streymi"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ef þú hringir mun þessu símtali í <xliff:g id="OTHER_APP">%1$s</xliff:g> ljúka."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Veldu hvernig hringt er"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Framsenda símtal með <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Forritaravalmynd fyrir fjarskipti"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Ekki er hægt að svara símtölum meðan á neyðarsímtali stendur."</string>
<string name="cancel" msgid="6733466216239934756">"Hætta við"</string>
+ <string name="back" msgid="6915955601805550206">"Til baka"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Eyrnatól"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Höfuðtól með snúru"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Hátalari"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ytra tæki"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Óþekkt"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streymir hljóði í annað tæki"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Leggja á"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Skipta hingað"</string>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 3f78593be..4a17d18c4 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Chiamate in sottofondo"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Chiamate disconnesse"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"App per telefono arrestate in modo anomalo"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming chiamata"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Se effettui questa chiamata, la chiamata di <xliff:g id="OTHER_APP">%1$s</xliff:g> verrà terminata."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Scegli come effettuare questa chiamata"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Reindirizza la chiamata utilizzando <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu sviluppatore telecomunicazioni"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Impossibile accettare una chiamata durante una chiamata di emergenza."</string>
<string name="cancel" msgid="6733466216239934756">"Annulla"</string>
+ <string name="back" msgid="6915955601805550206">"Indietro"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auricolare"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Cuffie con cavo"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Vivavoce"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Esterno"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Sconosciuto"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio all\'altro dispositivo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Riaggancia"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Passa qui"</string>
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index d1d1c70c7..05ec712b0 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"שיחות ברקע"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"שיחות שנותקו"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"אפליקציות טלפון שקרסו"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"העברת השיחה"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ביצוע השיחה הזו יסיים את השיחה ב-<xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"איך להתקשר?"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"ניתוב דרך <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"תפריט למפתחי מערכות תקשורת"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"אי אפשר לענות לשיחות אחרות בזמן שיחת חירום."</string>
<string name="cancel" msgid="6733466216239934756">"ביטול"</string>
+ <string name="back" msgid="6915955601805550206">"חזרה"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"אוזניה"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"אוזניות חוטיות"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"רמקול"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"מכשיר חיצוני"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"לא ידוע"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"הקול מושמע במכשיר אחר"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ניתוק"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"העברת השיחה בחזרה לטלפון"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 04269753a..19387ffb5 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"バックグラウンドでの通話"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"通話の切断"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"通話アプリがクラッシュしたとき"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"通話ストリーミング"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"この通話を発信すると、<xliff:g id="OTHER_APP">%1$s</xliff:g> の通話が終了します。"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"通話の発信方法を選択してください"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> を使用して通話をリダイレクト"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom デベロッパー メニュー"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"緊急通報中は、他の電話を受けることができません。"</string>
<string name="cancel" msgid="6733466216239934756">"キャンセル"</string>
+ <string name="back" msgid="6915955601805550206">"戻る"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"受話口"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"有線ヘッドセット"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"スピーカー"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"外部"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"不明"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"他のデバイスに音声をストリーミングしています"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"通話を終了"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"このデバイスに切り替える"</string>
</resources>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 3aaa73ed4..d56873f25 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ზარები ფონში"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"გათიშული ზარები"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ავარიულად გათიშული ტელეფონის აპები"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"ზარის სტრიმინგი"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ამ ზარის განხორციელება თქვენს <xliff:g id="OTHER_APP">%1$s</xliff:g> ზარს დაასრულებს."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"აირჩიეთ, როგორ განათავსოთ ეს ზარი"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"ზარის გადამისამართება <xliff:g id="OTHER_APP">%1$s</xliff:g>-ის გამოყენებით"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom-ის დეველოპერის მენიუ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"გადაუდებელი ზარის დროს ზარების მიღება შეუძლებელია."</string>
<string name="cancel" msgid="6733466216239934756">"გაუქმება"</string>
+ <string name="back" msgid="6915955601805550206">"უკან"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ყურმილი"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"სადენიანი ყურსაცვამი"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"დინამიკი"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"გარე"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"უცნობი"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"მიმდინარეობს აუდიოს სტრიმინგი სხვა მოწყობილობაზე"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"გათიშვა"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"გადართვა"</string>
</resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 2836b5d94..628b44025 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Фондық қоңыраулар"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Ажыратылған қоңыраулар"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Бұзылған телефон қолданбалары"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Қоңырауды трансляциялау"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Жаңа қоңырау шалу <xliff:g id="OTHER_APP">%1$s</xliff:g> қоңырауын тоқтатады."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Қоңырау шалу әдісін таңдаңыз."</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Қоңырау бағытын <xliff:g id="OTHER_APP">%1$s</xliff:g> арқылы ауыстыру"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom Developer мәзірі"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Құтқару қызметімен сөйлесіп жатқанда, басқа қоңырауларды қабылдай алмайсыз."</string>
<string name="cancel" msgid="6733466216239934756">"Бас тарту"</string>
+ <string name="back" msgid="6915955601805550206">"Артқа"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Динамик"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Сымды гарнитура"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Динамик"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Сыртқы"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Белгісіз"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Аудионы басқа құрылғыға трансляциялау"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Қоңырауды аяқтау"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Осы жерде ауысу"</string>
</resources>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index b00ab2b00..1c28d3711 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ការហៅនៅផ្ទៃខាងក្រោយ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ការហៅ​ទូរសព្ទដែលបាន​ផ្ដាច់"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"កម្មវិធី​ទូរសព្ទ​គាំង"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"មុខងារផ្សាយ​ការហៅទូរសព្ទ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ការ​ហៅ​ទូរសព្ទ​នេះ នឹង​បញ្ចប់​ការហៅ <xliff:g id="OTHER_APP">%1$s</xliff:g> របស់​អ្នក។"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ជ្រើសរើស​របៀប​ធ្វើ​ការហៅ​ទូរសព្ទ​នេះ"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"បញ្ជូន​ការហៅ​ទូរសព្ទ​បន្ត​ដោយ​ប្រើប្រាស់ <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ម៉ឺនុយ​អ្នក​អភិវឌ្ឍន៍​ទូរគមនាគមន៍"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"មិន​អាច​ទទួល​ការហៅ​ទូរសព្ទ​បាន​ទេ ពេល​កំពុង​ហៅ​ទៅ​លេខ​សង្គ្រោះ​បន្ទាន់។"</string>
<string name="cancel" msgid="6733466216239934756">"បោះបង់"</string>
+ <string name="back" msgid="6915955601805550206">"ថយក្រោយ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ឧបករណ៍ស្ដាប់សំឡេង"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ប៊្លូធូស"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"កាស​មាន​ខ្សែ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ឧបករណ៍​បំពង​សំឡេង"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ខាង​ក្រៅ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"មិន​ស្គាល់"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"កំពុង​ផ្សាយ​សំឡេង​ទៅឧបករណ៍​ផ្សេងទៀត"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"បញ្ចប់​ការហៅ​ទូរសព្ទ"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ប្ដូរនៅទីនេះ"</string>
</resources>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 885fc6580..cbaa2036b 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ಹಿನ್ನೆಲೆ ಕರೆಗಳು"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ಡಿಸ್ಕನೆಕ್ಟ್ ಮಾಡಲಾದ ಕರೆಗಳು"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ಕ್ರ್ಯಾಶ್ ಆಗಿರುವ ಫೋನ್ ಆ್ಯಪ್‌ಗಳು"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"ಕರೆ ಸ್ಟ್ರೀಮಿಂಗ್"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ಈ ಕರೆಯನ್ನು ಮಾಡುವುದರಿಂದ ನಿಮ್ಮ <xliff:g id="OTHER_APP">%1$s</xliff:g> ಕರೆಯು ಅಂತ್ಯಗೊಳ್ಳುತ್ತದೆ."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ಈ ಕರೆ ಮಾಡುವುದು ಹೇಗೆ ಎಂಬುದನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ಬಳಸಿಕೊಂಡು ಕರೆಯನ್ನು ಮರುನಿರ್ದೇರ್ಶಿಸಿ"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ಟೆಲಿಕಾಂ ಡೆವಲಪರ್ ಮೆನು"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ತುರ್ತು ಕರೆಯಲ್ಲಿರುವಾಗ ಕರೆಗಳನ್ನು ಸ್ವೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
<string name="cancel" msgid="6733466216239934756">"ರದ್ದುಮಾಡಿ"</string>
+ <string name="back" msgid="6915955601805550206">"ಹಿಂದಕ್ಕೆ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ಇಯರ್‌ಪೀಸ್"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ಬ್ಲೂಟೂತ್"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ವೈಯರ್ಡ್ ಹೆಡ್‌ಸೆಟ್"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ಸ್ಪೀಕರ್"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ಬಾಹ್ಯ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ಅಪರಿಚಿತ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ಇತರ ಸಾಧನಕ್ಕೆ ಆಡಿಯೊವನ್ನು ಸ್ಟ್ರೀಮ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ಹ್ಯಾಂಗ್ ಅಪ್"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ಇಲ್ಲಿಗೆ ಬದಲಾಯಿಸಿ"</string>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index f428191e5..dc793e320 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"백그라운드 통화"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"연결 해제된 통화"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"다운된 전화 앱"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"통화 스트리밍"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"이 전화를 걸면 현재 <xliff:g id="OTHER_APP">%1$s</xliff:g>에서 진행 중인 통화가 종료됩니다."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"전화 걸 방법 선택"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> 앱으로 전화 리디렉션"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom 개발자 메뉴"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"긴급 전화 중에는 전화를 받을 수 없습니다."</string>
<string name="cancel" msgid="6733466216239934756">"취소"</string>
+ <string name="back" msgid="6915955601805550206">"뒤로"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"스피커"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"블루투스"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"유선 헤드셋"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"스피커"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"외부"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"알 수 없음"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"다른 기기로 오디오 스트리밍"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"전화 끊기"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"현재 기기로 전환"</string>
</resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 31ffd48cb..43def8b66 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Фондогу чалуулар"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Ажыратылган чалуулар"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Катадан улам иштебей калган телефон колдонмолору"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Чалууну берүү"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Эгер чалып баштасаңыз, <xliff:g id="OTHER_APP">%1$s</xliff:g> чалууңуз аяктайт."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Чалуу жолун тандаңыз"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> аркылуу чалуу багытын буруу"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom иштеп чыгуучусунун менюсу"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Шашылыш учурунда чалуулар кабыл алынбайт."</string>
<string name="cancel" msgid="6733466216239934756">"Жокко чыгаруу"</string>
+ <string name="back" msgid="6915955601805550206">"Артка"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Кулакчын"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Зымдуу гарнитура"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Динамик"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Тышкы"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Белгисиз"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Аудио башка түзмөккө берилүүдө"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Чалууну бүтүрүү"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Бул жерге которулуу"</string>
</resources>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 6d176d27b..ff79144c1 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ການໂທໃນພື້ນຫຼັງ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ສາຍຖືກຕັດແລ້ວ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ແອັບໂທລະສັບຂັດຂ້ອງ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"ການສະຕຣີມການໂທ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ການໂທສາຍນີ້ຈະເປັນການສິ້ນສຸດສາຍ <xliff:g id="OTHER_APP">%1$s</xliff:g> ຂອງທ່ານ."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ເລືອກວິທີໂທສາຍນີ້"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"ປ່ຽນເສັ້ນທາງການໂທໂດຍໃຊ້ <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ເມນູນັກພັດທະນາໂທລະຄົມ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ບໍ່ສາມາດໂທໄດ້ໃນຂະນະທີ່ຢູ່ໃນການໂທສຸກເສີນ."</string>
<string name="cancel" msgid="6733466216239934756">"ຍົກເລີກ"</string>
+ <string name="back" msgid="6915955601805550206">"ກັບຄືນ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ຫູຟັງ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ຊຸດຫູຟັງແບບມີສາຍ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ລຳໂພງ"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ພາຍນອກ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ບໍ່ຮູ້ຈັກ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ສະຕຣີມສຽງໄປໃສ່ອຸປະກອນອື່ນ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ວາງສາຍ"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ສະຫຼັບບ່ອນນີ້"</string>
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index b53008957..945443122 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Skambučiai fone"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Skambučiai atjungti"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Užstrigusios telefono programos"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Srautinis skambučio perdavimas"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Atliekant šį skambutį bus užbaigtas „<xliff:g id="OTHER_APP">%1$s</xliff:g>“ skambutis."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Pasirinkite, kaip norite skambinti"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Peradresuoti skambutį naudojant programą „<xliff:g id="OTHER_APP">%1$s</xliff:g>“"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telekomunikacijų kūrėjų meniu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Atliekant skambutį pagalbos numeriu negalima atsiliepti į kitus skambučius."</string>
<string name="cancel" msgid="6733466216239934756">"Atšaukti"</string>
+ <string name="back" msgid="6915955601805550206">"Atgal"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Garsiakalbis prie ausies"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Laidinės ausinės"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Garsiakalbis"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Išoriniai šaltiniai"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Nežinoma"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Srautinis garso perdavimas į kitą įrenginį"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Baigti skambutį"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Perjungti čia"</string>
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 4219487ea..5ebdd8eb2 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Sarunas fonā"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Pārtrauktie zvani"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Avarējušās tālruņa lietotnes"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Zvana straumēšana"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Veicot šo zvanu, tiks beigts zvans lietotnē <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Izvēlieties, kā veikt šo zvanu"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Novirzīt zvanu, izmantojot lietotni <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom izstrādātāja izvēlne"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Ārkārtas izsaukuma laikā nevar pieņemt zvanus."</string>
<string name="cancel" msgid="6733466216239934756">"Atcelt"</string>
+ <string name="back" msgid="6915955601805550206">"Atpakaļ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auss skaļrunis"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Vadu austiņas"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Skaļrunis"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ārēja ierīce"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Nezināma ierīce"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Notiek audio straumēšana uz citu ierīci."</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Beigt zvanu"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Pārslēgties šeit"</string>
</resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index f48e83190..57a3fce6c 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Повици во заднина"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Прекинати повици"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Паднати апликации за телефон"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Стримување повик"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ако се воспостави повиков, вашиот повик на <xliff:g id="OTHER_APP">%1$s</xliff:g> ќе заврши."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Изберете како да се воспостави повиков"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Пренасочи го повикот со <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Програмерско мени за телекомуникации"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Не може да примате повици во тек на итен повик."</string>
<string name="cancel" msgid="6733466216239934756">"Откажи"</string>
+ <string name="back" msgid="6915955601805550206">"Назад"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Слушалка"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Жичени слушалки"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Звучник"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Надворешно"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Непознато"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Звукот се стримува на друг уред"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Спушти"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Префрли овде"</string>
</resources>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index beb730d2c..a6d1626d9 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"പശ്ചാത്തല കോളുകൾ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"വിച്ഛേദിക്കപ്പെട്ട കോളുകൾ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ക്രാഷായ ഫോൺ ആപ്പുകൾ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"കോൾ സ്ട്രീമിംഗ്"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ഈ കോൾ ചെയ്യുന്നത് നിങ്ങളുടെ <xliff:g id="OTHER_APP">%1$s</xliff:g> കോൾ അവസാനിക്കാനിടയാക്കും."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ഈ കോൾ എങ്ങനെ ചെയ്യണമെന്ന് തിരഞ്ഞെടുക്കുക"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ഉപയോഗിച്ച് കോൾ റീഡയറക്‌റ്റ് ചെയ്യുക"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ടെലികോം ഡെവലപ്പര്‍ മെനു"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"അടിയന്തര കോളിലായിരിക്കുമ്പോൾ കോളുകൾ എടുക്കാനാവില്ല."</string>
<string name="cancel" msgid="6733466216239934756">"റദ്ദാക്കുക"</string>
+ <string name="back" msgid="6915955601805550206">"മടങ്ങുക"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ഇയർഫോൺ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"വയേർഡ് ഹെഡ്‌സെറ്റ്"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"സ്പീക്കർ"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"എക്സ്റ്റേണൽ സ്‌ട്രീമിംഗ്"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"അജ്ഞാതം"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ഓഡിയോ മറ്റൊരു ഉപകരണത്തിലേക്ക് സ്‌ട്രീം ചെയ്യുന്നു"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"മാറ്റി വയ്‌ക്കുക"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ഇവിടേക്ക് മാറുക"</string>
</resources>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 71231dc85..70dde8a44 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Арын дуудлагууд"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Салсан дуудлагууд"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Гэмтсэн гар утасны аппууд"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Дуудлага дамжуулах"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Энэ дуудлагыг хийснээр таны <xliff:g id="OTHER_APP">%1$s</xliff:g> дуудлагыг дуусгана."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Энэ дуудлагыг хэрхэн хийхийг сонгох"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g>-г ашиглан дуудлагыг дахин чиглүүлэх"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Телеком хөгжүүлэгчийн цэс"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Яаралтай дуудлагын үеэр дуудлага авах боломжгүй."</string>
<string name="cancel" msgid="6733466216239934756">"Цуцлах"</string>
+ <string name="back" msgid="6915955601805550206">"Буцах"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Чихний спикер"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Утастай чихэвч"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Чанга яригч"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Гадны"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Тодорхойгүй"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Бусад төхөөрөмж рүү аудио дамжуулж байна"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Таслах"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Ийшээ сэлгэх"</string>
</resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index dfff80f6f..c4438aebc 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"बॅकग्राउंड कॉल"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"डिस्कनेक्ट केलेले कॉल"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"क्रॅश झालेली फोन ॲप्स"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"कॉल स्ट्रीमिंग"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"हा कॉल केल्याने तुमचा <xliff:g id="OTHER_APP">%1$s</xliff:g> कॉल समाप्त होईल."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"हा कॉल कसा करायचा ते निवडा"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> वापरून कॉल रीडिरेक्ट करा"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"टेलिकॉम डेव्‍हलपर मेनू"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"आणीबाणी कॉल दरम्यान कॉल घेतला जाऊ शकत नाही."</string>
<string name="cancel" msgid="6733466216239934756">"रद्द करा"</string>
+ <string name="back" msgid="6915955601805550206">"मागे जा"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"इअरपिस"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ब्लूटूथ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"वायर्ड हेडसेट"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"स्पीकर"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"बाह्य"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"अज्ञात"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ऑडिओ हा दुसऱ्या डिव्हाइसवर स्ट्रीम करत आहे"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"बंद करा"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"येथे स्विच करा"</string>
</resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 0c5b1dc47..355502cc4 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Panggilan latar belakang"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Panggilan diputuskan sambungan"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplikasi telefon yang ranap"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Penstriman panggilan"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Membuat panggilan ini akan menamatkan panggilan <xliff:g id="OTHER_APP">%1$s</xliff:g> anda."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Pilih cara untuk membuat panggilan ini"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Ubah hala panggilan menggunakan <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu Pembangun Telekom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Panggilan tidak boleh dijawab semasa dalam panggilan kecemasan."</string>
<string name="cancel" msgid="6733466216239934756">"Batal"</string>
+ <string name="back" msgid="6915955601805550206">"Kembali"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Alat dengar"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Set kepala berwayar"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Pembesar suara"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Luaran"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Tidak diketahui"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Penstriman audio pada peranti lain"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Tamatkan panggilan"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Tukar di sini"</string>
</resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7a5dbbfeb..9ead5f410 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"နောက်ခံမှ ခေါ်ဆိုမှုများ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ပြတ်တောက်သွားသည့် ခေါ်ဆိုမှုများ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ရပ်တန့်သွားသော ဖုန်းအက်ပ်များ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"ခေါ်ဆိုမှု တိုက်ရိုက်လွှင့်ခြင်း"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ဤခေါ်ဆိုမှု ပြုလုပ်ပါက <xliff:g id="OTHER_APP">%1$s</xliff:g> သုံးပြီးပြောနေခြင်းကို ဖြတ်ပစ်ပါမည်။"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ဤခေါ်ဆိုမှု ပြုလုပ်ပုံကို ရွေးချယ်ပါ"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"ခေါ်ဆိုမှုကို <xliff:g id="OTHER_APP">%1$s</xliff:g> ဖြင့် တစ်ဆင့်ပြန်ညွှန်ရန်"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom ဆော့ဖ်ဝဲအင်ဂျင်နီယာ မီနူး"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"အရေးပေါ်ခေါ်ဆိုမှု ပြုလုပ်နေစဉ် ဖုန်းမခေါ်နိုင်ပါ။"</string>
<string name="cancel" msgid="6733466216239934756">"မလုပ်တော့"</string>
+ <string name="back" msgid="6915955601805550206">"နောက်သို့"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"တယ်လီဖုန်းနားခွက်"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ဘလူးတုသ်"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ကြိုးတပ် မိုက်ခွက်ပါနားကြပ်"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"စပီကာ"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ပြင်ပ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"မသိ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"အသံကို အခြားစက်တွင် တိုက်ရိုက်လွှင့်နေသည်"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ဖုန်းချရန်"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ဤနေရာသို့ လွှဲပြောင်းရန်"</string>
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 77331cef5..8bebbff63 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Bakgrunnsanrop"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Frakoblede anrop"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Telefonapper som har krasjet"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Anropsstrømming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Samtalen din i <xliff:g id="OTHER_APP">%1$s</xliff:g> avsluttes hvis du foretar dette anropet."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Velg hvordan du vil ringe"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Viderekoble anropet med <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Meny for telekommunikasjonsutviklere"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Du kan ikke besvare anrop mens du har et pågående nødanrop."</string>
<string name="cancel" msgid="6733466216239934756">"Avbryt"</string>
+ <string name="back" msgid="6915955601805550206">"Gå tilbake"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Høyttaler (ørestykke)"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Hodetelefoner med ledning"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Høyttaler"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ekstern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Ukjent"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Strømmer lyden til en annen enhet"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Legg på"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Flytt hit"</string>
</resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index cdf6c1f6b..44645dc8c 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ब्याकग्राउन्डका कलहरू"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"विच्छेद गरिएका कल"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"फोनमा रहेका क्र्यास भएका एपहरू"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"कल स्ट्रिमिङ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"यो कल गर्नुले तपाईंको <xliff:g id="OTHER_APP">%1$s</xliff:g> कल अन्त्य गर्दछ।"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"यो कल गर्ने तरिका छनौट गर्नुहोस्"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> प्रयोग गरी कल रिडाइरेक्ट गर्नुहोस्"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"टेलिकमको विकासकर्ताको मेनु"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"आपत्‌कालीन कल चलिराखेको बेलामा अरु कल स्वीकार गर्न सकिँदैन।"</string>
<string name="cancel" msgid="6733466216239934756">"रद्द गर्नुहोस्"</string>
+ <string name="back" msgid="6915955601805550206">"पछाडि"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"इयरपस"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ब्लुटुथ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"तारसहितको हेडसेट"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"स्पिकर"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"बाह्य"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"अज्ञात"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"अर्को डिभाइसमा अडियो स्ट्रिम गरिँदै छ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"कल काट्नुहोस्"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"यहाँ गई बदल्नुहोस्"</string>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 3e7b07128..8dfee8189 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Achtergrondgesprekken"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Beëindigde gesprekken"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Gecrashte telefoon-apps"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Oproepstreaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Als je dit gesprek start, wordt je <xliff:g id="OTHER_APP">%1$s</xliff:g>-gesprek beëindigd."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Kies hoe je dit gesprek wilt plaatsen"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Gesprek omleiden via <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecomontwikkelaarsmenu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Gesprekken kunnen niet worden aangenomen tijdens een noodoproep."</string>
<string name="cancel" msgid="6733466216239934756">"Annuleren"</string>
+ <string name="back" msgid="6915955601805550206">"Terug"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Oortelefoon"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Bedrade headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Extern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Onbekend"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio streamen naar ander apparaat"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Ophangen"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Hiernaartoe schakelen"</string>
</resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 8b52919aa..787711b88 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ବ୍ୟାକ୍‌ଗ୍ରାଉଣ୍ଡ କଲ୍‌ଗୁଡ଼ିକ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ବିଚ୍ଛିନ୍ନ କରାଯାଇଥିବା କଲ୍‌ଗୁଡ଼ିକ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"କ୍ରାସ୍ ହୋଇଥିବା ଫୋନ୍ ଆପ୍ସ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"କଲ ଷ୍ଟ୍ରିମିଂ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ଏହି କଲ୍‌କୁ ସ୍ଥାପନ କରିବା ଦ୍ଵାରା ଆପଣଙ୍କର <xliff:g id="OTHER_APP">%1$s</xliff:g> କଲ୍ ସମାପ୍ତ ହୋ‌ଇଯିବ।"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ଏହି କଲ୍ କିପରି କରିବାକୁ ଚାହାନ୍ତି ବାଛନ୍ତୁ"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ବ୍ୟବହାର କରି କଲ୍ ରିଡାଇରେକ୍ଟ କରନ୍ତୁ"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ଟେଲେକମ୍ ଡେଭେଲପର୍ ମେନୁ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ଜରୁରୀକାଳୀନ କଲ୍ ବେଳେ ଅନ୍ୟ କଲ୍ ଉଠାଇ ପାରିବେ ନାହିଁ।"</string>
<string name="cancel" msgid="6733466216239934756">"ବାତିଲ କରନ୍ତୁ"</string>
+ <string name="back" msgid="6915955601805550206">"ପଛକୁ ଫେରନ୍ତୁ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ଇୟରପିସ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ବ୍ଲୁଟୁଥ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ତାରଯୁକ୍ତ ହେଡସେଟ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ସ୍ପିକର"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ଏକ୍ସଟର୍ନଲ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ଅଜଣା"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ଅନ୍ୟ ଡିଭାଇସରେ ଅଡିଓ ଷ୍ଟ୍ରିମ କରାଯାଉଛି"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"କଲ ସମାପ୍ତ କରନ୍ତୁ"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ଏଠାରେ ସୁଇଚ କରନ୍ତୁ"</string>
</resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 351cf151f..b96a1dbe4 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"ਬੈਕਗ੍ਰਾਊਂਡ ਕਾਲਾਂ"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"ਡਿਸਕਨੈਕਟ ਕੀਤੀਆਂ ਕਾਲਾਂ"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"ਕ੍ਰੈਸ਼ ਹੋਈਆਂ ਫ਼ੋਨ ਐਪਾਂ"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"ਕਾਲ ਸਟ੍ਰੀਮਿੰਗ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ਇਹ ਕਾਲ ਕਰਨ ਨਾਲ ਤੁਹਾਡੀ <xliff:g id="OTHER_APP">%1$s</xliff:g> ਕਾਲ ਸਮਾਪਤ ਹੋ ਜਾਵੇਗੀ।"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ਚੁਣੋ ਕਿ ਕਾਲ ਕਿਵੇਂ ਕਰਨੀ ਹੈ"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ਦੀ ਵਰਤੋਂ ਕਰਕੇ ਕਾਲ ਰੀਡਾਇਰੈਕਟ ਕਰੋ"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ਟੈਲੀਕੋਮ ਵਿਕਾਸਕਾਰ ਮੀਨੂ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ਕਿਸੇ ਸੰਕਟਕਾਲੀਨ ਕਾਲ ਦੌਰਾਨ ਹੋਰ ਕਾਲਾਂ ਨਹੀਂ ਲਈਆਂ ਜਾ ਸਕਦੀਆਂ।"</string>
<string name="cancel" msgid="6733466216239934756">"ਰੱਦ ਕਰੋ"</string>
+ <string name="back" msgid="6915955601805550206">"ਪਿੱਛੇ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ਈਅਰਪੀਸ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"ਬਲੂਟੁੱਥ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ਤਾਰ ਵਾਲਾ ਹੈੱਡਸੈੱਟ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ਸਪੀਕਰ"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ਬਾਹਰੀ"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ਅਗਿਆਤ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ਆਡੀਓ ਨੂੰ ਕਿਸੇ ਹੋਰ ਡੀਵਾਈਸ \'ਤੇ ਸਟ੍ਰੀਮ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ਕਾਲ ਸਮਾਪਤ ਕਰੋ"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ਇੱਥੇ ਸਵਿੱਚ ਕਰੋ"</string>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index caf5fd70d..df5d29eb1 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Połączenia w tle"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Przerwane połączenia"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplikacje telefoniczne po awarii"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Strumieniowanie połączenia"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Jeśli zadzwonisz, połączenie w aplikacji <xliff:g id="OTHER_APP">%1$s</xliff:g> zostanie zakończone."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Wybierz, jak chcesz zadzwonić"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Przekieruj połączenie za pomocą aplikacji <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu programisty Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Nie można odbierać rozmów przy nawiązanym połączeniu alarmowym."</string>
<string name="cancel" msgid="6733466216239934756">"Anuluj"</string>
+ <string name="back" msgid="6915955601805550206">"Wstecz"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Słuchawka"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Słuchawki przewodowe"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Głośnik"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Zewnętrzne"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Brak informacji"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Strumieniowanie dźwięku na inne urządzenie"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Rozłącz"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Przełącz tutaj"</string>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a1e725e08..5fbe1d3aa 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -65,7 +65,7 @@
<string name="blocked_numbers" msgid="8322134197039865180">"Números bloqueados"</string>
<string name="blocked_numbers_msg" msgid="2797422132329662697">"Não irá receber chamadas ou mensagens de texto de números bloqueados."</string>
<string name="block_number" msgid="3784343046852802722">"Adicionar um número"</string>
- <string name="unblock_dialog_body" msgid="2723393535797217261">"Pretende desbloquear <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
+ <string name="unblock_dialog_body" msgid="2723393535797217261">"Quer desbloquear <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
<string name="unblock_button" msgid="8732021675729981781">"Desbloquear"</string>
<string name="add_blocked_dialog_body" msgid="8599974422407139255">"Bloquear chamadas e mensagens de texto de"</string>
<string name="add_blocked_number_hint" msgid="8769422085658041097">"Número de telefone"</string>
@@ -100,8 +100,9 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Chamadas em segundo plano"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Chamadas desligadas"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Apps Telefone com falhas"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming de chamadas"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ao efetuar esta chamada, irá terminar a chamada na app <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
- <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Escolha como pretende efetuar esta chamada"</string>
+ <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Escolha como quer efetuar esta chamada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirecionar chamada através de <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
<string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Ligar com o meu número de telefone"</string>
<string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Não é possível efetuar uma chamada através da app <xliff:g id="OTHER_APP">%1$s</xliff:g>. Experimente utilizar uma app de redirecionamento de chamadas diferente ou contactar o programador para obter ajuda."</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu do programador de telecomunicações"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Não é possível atender chamadas durante uma chamada de emergência."</string>
<string name="cancel" msgid="6733466216239934756">"Cancelar"</string>
+ <string name="back" msgid="6915955601805550206">"Anterior"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Auricular"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Auscultadores com microfone integrado com fios"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Altifalante"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externo"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Desconhecido"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"A fazer stream de áudio para outro dispositivo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Desligar"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Mudar aqui"</string>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 10b51b4dd..afb8eca46 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Chamadas em segundo plano"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Chamadas desconectadas"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Falha com os apps de telefone"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming de ligação"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Se você ligar agora, sua chamada será encerrada no <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Escolha como fazer esta chamada"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirecionar a chamada usando o <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu do desenvolvedor de telecomunicação"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Durante uma chamada de emergência, não é possível transferir chamadas para o dispositivo."</string>
<string name="cancel" msgid="6733466216239934756">"Cancelar"</string>
+ <string name="back" msgid="6915955601805550206">"Voltar"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Minifone de ouvido"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Fone de ouvido com fio"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Alto-falante"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externo"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Desconhecido"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Fazendo streaming de áudio para outro dispositivo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Desligar"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Mudar para este dispositivo"</string>
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 8b4191e61..8e485d019 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Apeluri în fundal"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Apeluri deconectate"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplicații pentru telefon blocate"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streaming de apeluri"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Dacă inițiezi acest apel, cel din <xliff:g id="OTHER_APP">%1$s</xliff:g> va fi încheiat."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Alege cum vrei să inițiezi apelul"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Redirecționezi apelul folosind <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Meniu pentru dezvoltatori de telecomunicații"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Nu poți răspunde la apeluri în timpul unui apel de urgență."</string>
<string name="cancel" msgid="6733466216239934756">"Anulează"</string>
+ <string name="back" msgid="6915955601805550206">"Înapoi"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Cască"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Set de căști-microfon cu fir"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Difuzor"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Extern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Necunoscut"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio pe alt dispozitiv"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Încheie apelul"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Treci la alt cont aici"</string>
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 78f5a6c76..67ab2e904 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Фоновые вызовы"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Прекращенные вызовы"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Приложения для телефона, работа которых прекращена из-за ошибки"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Потоковая передача звонков"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Если вы начнете этот звонок, вызов в <xliff:g id="OTHER_APP">%1$s</xliff:g> будет завершен."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Выберите, как хотите позвонить."</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Перенаправить вызов с использованием <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Меню разработчика Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Невозможно принять вызов, когда уже выполняется экстренный вызов."</string>
<string name="cancel" msgid="6733466216239934756">"Отмена"</string>
+ <string name="back" msgid="6915955601805550206">"Назад"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Динамик телефона"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Проводная гарнитура"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Динамик"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Внешнее устройство"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Неизвестно"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Потоковая передача аудио на другое устройство"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Завершить"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Переключиться"</string>
</resources>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 8ccff4a7f..71442e0ea 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"පසුබිම් ඇමතුම්"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"විසන්ධි කළ ඇමතුම්"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"බිඳ වැටුණු දුරකථන යෙදුම්"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"ඇමතුම් ප්‍රවාහය"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"මෙම ඇමතුම ගැනීම ඔබේ <xliff:g id="OTHER_APP">%1$s</xliff:g> ඇමතුම අවසන් කරනු ඇත."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"මෙම ඇමතුම ගන්නා ආකාරය තෝරන්න"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> භාවිතයෙන් ඇමතුම ප්‍රතියොමු කරන්න"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ටෙලිකොම් සංවර්ධක මෙනුව"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"හදිසි ඇමතුමක් අතරතුර ඇමතුම් ගත නොහැකිය."</string>
<string name="cancel" msgid="6733466216239934756">"අවලංගු කරන්න"</string>
+ <string name="back" msgid="6915955601805550206">"ආපසු"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"සවන් කඩ"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"බ්ලූටූත්"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"රැහැන්ගත කළ හෙඩ්සෙට්"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ස්පීකරය"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"බාහිර"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"නොදනී"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"වෙනත් උපාංගයකට ශ්‍රව්‍ය ප්‍රවාහ කිරීම"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"විසන්ධි කරන්න"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"මෙතැනට මාරු වෙන්න"</string>
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 38e7417c1..a001130b1 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Hovory na pozadí"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Zrušené hovory"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Zrútené telefónne aplikácie"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Streamovanie hovoru"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ak uskutočníte tento hovor, hovor cez <xliff:g id="OTHER_APP">%1$s</xliff:g> bude ukončený."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Vyberte, ako chcete tento hovor uskutočniť"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Presmerovať hovor cez aplikáciu <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Ponuka pre vývojárov Telecomu"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Počas tiesňového volania nie je možné prijímať hovory."</string>
<string name="cancel" msgid="6733466216239934756">"Zrušiť"</string>
+ <string name="back" msgid="6915955601805550206">"Späť"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Slúchadlo"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Káblové slúchadlo s mikrofónom"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Reproduktor"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Externé"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Neznáme"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streamovanie zvuku do iného zariadenia"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Zložiť"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Prepnúť sem"</string>
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index edc41a30f..994bc7e35 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Klici v ozadju"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Prekinjeni klici"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Zrušene aplikacije za klicanje"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Pretočno predvajanje klicev"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Če opravite ta klic, bo končan klic prek aplikacije <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Izberite, kako želite opraviti klic"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Preusmeri klic z aplikacijo <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Meni za razvijalce Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Med klicem v sili ni mogoče sprejeti klicev."</string>
<string name="cancel" msgid="6733466216239934756">"Prekliči"</string>
+ <string name="back" msgid="6915955601805550206">"Nazaj"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Slušalka"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Žične slušalke z mikrofonom"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Zvočnik"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Zunanje"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Neznano"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Pretočno predvajanje zvoka v drugo napravo"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini klic"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Preklopi sem"</string>
</resources>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 80d1d7787..89ae852a1 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Telefonatat në sfond"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Telefonatat e shkëputura"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Aplikacionet e telefonit që kanë pësuar ndërprerje aksidentale"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Transmetimi i telefonatave"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Kryerja e kësaj telefonate do të mbyllë telefonatën tënde në <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Zgjidh se si do ta kryesh këtë telefonatë"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Ridrejtoje telefonatën duke përdorur <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menyja e zhvilluesit të telekomunikimit"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Nuk mund të marrësh telefonata kur je në një telefonatë urgjence."</string>
<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_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>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"E panjohur"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Po transmetohet audioja te një pajisje tjetër"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Mbyll"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Ndërro këtu"</string>
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 617f8d830..113438003 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Позиви у позадини"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Прекинути позиви"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Апликације за телефонирање које су отказале"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Стримовање позива"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ако упутите овај позив, завршићете <xliff:g id="OTHER_APP">%1$s</xliff:g> позив."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Изаберите како желите да упутите овај позив"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Преусмери позив помоћу: <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Мени за програмере Telecom-а"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"За време хитног позива није могуће преузимати друге позиве."</string>
<string name="cancel" msgid="6733466216239934756">"Откажи"</string>
+ <string name="back" msgid="6915955601805550206">"Назад"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Слушалица"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Жичане слушалице"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Звучник"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Екстерни"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Непознато"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Звук се стримује на други уређај"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Прекини везу"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Пребаци овде"</string>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 41d4d1611..c6f6ec9e3 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Bakgrundssamtal"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Frånkopplade samtal"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Kraschade telefonappar"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Samtalsstreaming"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ringer du det här samtalet avslutas samtalet i <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Välj hur du vill ringa samtalet"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Omdirigera samtal med <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Meny för telekomutvecklare"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Det går inte att besvara samtal medan ett nödsamtal pågår."</string>
<string name="cancel" msgid="6733466216239934756">"Avbryt"</string>
+ <string name="back" msgid="6915955601805550206">"Tillbaka"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Lur"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Kabelanslutet headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Högtalare"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Extern"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Okänd"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Streama ljud till en annan enhet"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Lägg på"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Koppla hit"</string>
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index e6be099dc..1b499900c 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Simu za chinichini"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Simu zilizokatwa"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Programu za simu zilizoacha kufanya kazi"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Utiririshaji wa simu"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ukipiga simu hii, simu yako kwenye <xliff:g id="OTHER_APP">%1$s</xliff:g> itakatwa."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Chagua jinsi utakavyopiga simu hii"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Elekeza simu ukitumia <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menyu ya Msanidi programu wa Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Huwezi kupokea simu nyingine wakati unashiriki katika simu ya dharura."</string>
<string name="cancel" msgid="6733466216239934756">"Ghairi"</string>
+ <string name="back" msgid="6915955601805550206">"Rudi nyuma"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Spika ya sikioni"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Vifaa vya sauti vyenye waya"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Spika"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Ya nje"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Haijulikani"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Inatiririsha sauti kwenye kifaa kingine"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kata simu"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Badili hapa"</string>
</resources>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 3b812eaa9..9f37d8760 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"பின்னணி அழைப்புகள்"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"துண்டிக்கப்பட்ட அழைப்புகள்"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"சிதைவடைந்த மொபைல் ஆப்ஸ்"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"அழைப்பு ஸ்ட்ரீமிங்"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"புதிய அழைப்பைச் செய்தால், செயலில் உள்ள <xliff:g id="OTHER_APP">%1$s</xliff:g> அழைப்பு துண்டிக்கப்படும்."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"இந்த அழைப்பை எவ்வாறு மேற்கொள்ள வேண்டும் எனத் தேர்ந்தெடுக்கவும்"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g>ஐப் பயன்படுத்தி அழைப்பைத் திருப்பி விடு"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"டெலிகாம் டெவெலப்பர் மெனு"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"அவசர அழைப்பின்போது அழைப்புகளை ஏற்க முடியாது."</string>
<string name="cancel" msgid="6733466216239934756">"ரத்துசெய்"</string>
+ <string name="back" msgid="6915955601805550206">"பின்செல்"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ஒலி கேட்கும் பகுதி"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"புளூடூத்"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"வயர் ஹெட்செட்"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ஸ்பீக்கர்"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"வெளிப்புறச் சாதனம்"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"தெரியவில்லை"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"வேறு சாதனத்திற்கு ஆடியோவை ஸ்ட்ரீம் செய்கிறது"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"அழைப்பைத் துண்டி"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"இங்கே மாற்று"</string>
</resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index edd0d9594..8f8a23e4d 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"బ్యాక్‌గ్రౌండ్ కాల్స్"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"డిస్‌కనెక్ట్ చేసిన కాల్స్"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"క్రాష్ అయిన ఫోన్ యాప్స్"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"కాల్ స్ట్రీమింగ్"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"ఈ కాల్ చేయడం వలన మీ <xliff:g id="OTHER_APP">%1$s</xliff:g> కాల్ ముగుస్తుంది."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"ఈ కాల్ ఎలా చేయాలో ఎంచుకోండి"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> ఉపయోగించి కాల్ మళ్లించు"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"టెలికామ్ డెవలపర్ మెనూ"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"అత్యవసర కాల్‌లో వున్నప్పుడు కాల్స్‌ను స్వీకరించడానికి వీలుపడదు."</string>
<string name="cancel" msgid="6733466216239934756">"రద్దు చేయండి"</string>
+ <string name="back" msgid="6915955601805550206">"వెనుకకు"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ఇయర్‌పీస్"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"బ్లూటూత్"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"వైర్ ఉన్న హెడ్‌సెట్"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"స్పీకర్"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"వెలుపలి"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"తెలియదు"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"ఆడియోను ఇతర పరికరానికి స్ట్రీమింగ్ చేయండి"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ముగించండి"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ఇక్కడకు స్విచ్ అవ్వండి"</string>
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index f09de8632..b8dc9f0d2 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"การโทรในเบื้องหลัง"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"สายถูกตัด"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"แอปโทรศัพท์ขัดข้อง"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"การสตรีมการโทร"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"การโทรออกนี้จะวางสายใน <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"เลือกวิธีโทรออก"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"โอนสายโดยใช้ <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"เมนูนักพัฒนาโทรคมนาคม"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"การโทรนั้นจะทำขณะอยู่ในการโทรฉุกเฉินไม่ได้"</string>
<string name="cancel" msgid="6733466216239934756">"ยกเลิก"</string>
+ <string name="back" msgid="6915955601805550206">"กลับ"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"หูฟังโทรศัพท์"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"บลูทูธ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"ชุดหูฟังแบบมีสาย"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"ลำโพง"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"ภายนอก"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"ไม่ทราบ"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"กำลังสตรีมเสียงไปยังอุปกรณ์อื่นๆ"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"วางสาย"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"เปลี่ยนที่นี่"</string>
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index a4d519688..91e1b3323 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Mga tawag sa background"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Nadiskonektang mga tawag"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Nag-crash na mga phone app"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Pag-stream ng tawag"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Tatapusin ng pagtawag na ito ang iyong tawag sa <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Piliin kung paano gagawin ang tawag na ito"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"I-redirect ang tawag gamit ang <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu ng Telecom Developer"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Hindi puwedeng sumagot ng mga tawag habang nasa emergency na tawag."</string>
<string name="cancel" msgid="6733466216239934756">"Kanselahin"</string>
+ <string name="back" msgid="6915955601805550206">"Bumalik"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Earpiece"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Wired na headset"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Speaker"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"External"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Hindi Alam"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Naka-stream ang audio sa ibang device"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Mag-hang up"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Lumipat dito"</string>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index da9416abe..0aa2e20d0 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Arka plandaki aramalar"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Bağlantısı kesilen aramalar"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Kilitlenen telefon uygulamaları"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Görüşme aktarımı"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Bu çağrıyı yaptığınızda <xliff:g id="OTHER_APP">%1$s</xliff:g> çağrınız sona erecek."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Bu aramanın nasıl yapılacağını seçin"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> uygulamasını kullanarak aramayı yönlendir"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telekomünikasyon Geliştirici Menüsü"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Acil durum araması sırasında arama alınamaz."</string>
<string name="cancel" msgid="6733466216239934756">"İptal"</string>
+ <string name="back" msgid="6915955601805550206">"Geri"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Kulaklık"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Kablolu mikrofonlu kulaklık"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Hoparlör"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Harici"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Bilinmiyor"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Ses başka bir cihaza aktarılıyor"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Görüşmeyi bitir"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Buraya dön"</string>
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 551632a4a..a4d01d175 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Виклики у фоновому режимі"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Припинені виклики"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Збої в додатках для дзвінків"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Трансляція дзвінків"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Якщо здійснити цей виклик, буде завершено виклик у додатку <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Виберіть, як здійснити цей виклик"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Переспрямувати через додаток <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Меню розробника Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Під час екстрених викликів не можна приймати інші."</string>
<string name="cancel" msgid="6733466216239934756">"Скасувати"</string>
+ <string name="back" msgid="6915955601805550206">"Назад"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Динамік"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Дротова гарнітура"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Гучний зв’язок"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Зовнішні джерела"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Невідомо"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Аудіо транслюється на інший пристрій"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Завершити"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Перевести сюди"</string>
</resources>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 146d72044..6649f4200 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"پس منظر کی کالز"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"منقطع کالز"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"کریشڈ فون ایپس"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"کال اسٹریمنگ"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"یہ کال کرنے سے <xliff:g id="OTHER_APP">%1$s</xliff:g> کال ختم ہو جائے گی۔"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"یہ کال کرنے کا طریقہ منتخب کریں"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"<xliff:g id="OTHER_APP">%1$s</xliff:g> کے ذریعے کال کو ریڈائریکٹ کریں"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"ٹیلی کام ڈویلپر مینیو"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"ایمرجنسی کال کے دوران کالز نہیں لی جائیں گی۔"</string>
<string name="cancel" msgid="6733466216239934756">"منسوخ کریں"</string>
+ <string name="back" msgid="6915955601805550206">"پیچھے"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"ایئر پیس"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"بلوٹوتھ"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"تار والا ہیڈسیٹ"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"اسپیکر"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"خارجی"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"نامعلوم"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"دوسرے آلے پر آڈیو کی سلسلہ بندی کی جا رہی ہے"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"منقطع کریں"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"یہاں سوئچ کریں"</string>
</resources>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 0672e3613..c6805ea8b 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Orqa fondagi chaqiruvlar"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Tugatilgan chaqiruvlar"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Ishdan chiqqan telefon ilovalari"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Chaqiruv translatsiyasi"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Bu qo‘ng‘iroqni amalga oshirsangiz, <xliff:g id="OTHER_APP">%1$s</xliff:g> qo‘ng‘irog‘i tugatiladi."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Telefon qilish usulini tanlang"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Chaqiruv <xliff:g id="OTHER_APP">%1$s</xliff:g> orqali qayta uzatilsin"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Telecom dasturchisi menyusi"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Favqulodda chaqiruv vaqtida boshqa chaqiruvlarni qabul qilish imkonsiz."</string>
<string name="cancel" msgid="6733466216239934756">"Bekor qilish"</string>
+ <string name="back" msgid="6915955601805550206">"Orqaga"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Quloq karnaychasi"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Simli garnitura"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Karnay"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Tashqi"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Noaniq"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio translatsiyani boshqa qurilmaga olish"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Tugatish"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Shu yerga olish"</string>
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 7481c22ab..5ae2e7907 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Cuộc gọi trong nền"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Các cuộc gọi bị ngắt kết nối"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Các ứng dụng điện thoại bị lỗi"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Truyền trực tuyến cuộc gọi"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Thực hiện cuộc gọi này sẽ kết thúc cuộc gọi <xliff:g id="OTHER_APP">%1$s</xliff:g> của bạn."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Chọn cách thực hiện cuộc gọi này"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Chuyển hướng cuộc gọi bằng cách sử dụng <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Menu nhà phát triển dịch vụ viễn thông"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Bạn không thể gọi điện trong khi thực hiện cuộc gọi khẩn cấp."</string>
<string name="cancel" msgid="6733466216239934756">"Hủy"</string>
+ <string name="back" msgid="6915955601805550206">"Quay lại"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Loa tai nghe"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"Tai nghe có dây"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Loa"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Bên ngoài"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Không xác định"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển đổi tại đây"</string>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 2cbdd7544..1ef0a552f 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -45,7 +45,7 @@
<string name="respond_via_sms_edittext_dialog_title" msgid="6579353156073272157">"快速回复"</string>
<string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"讯息已发送至 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string>
<string name="respond_via_sms_failure_format" msgid="5198680980054596391">"未能将信息发送到 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string>
- <string name="enable_account_preference_title" msgid="6949224486748457976">"通话帐号"</string>
+ <string name="enable_account_preference_title" msgid="6949224486748457976">"通话账号"</string>
<string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"只能拨打紧急呼救电话。"</string>
<string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"此应用没有电话权限,无法拨出电话。"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"要拨打电话,请输入有效的电话号码。"</string>
@@ -90,7 +90,7 @@
<string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"如果接听此来电,您当前的视频通话会中断。"</string>
<string name="answer_incoming_call" msgid="2045888814782215326">"接听"</string>
<string name="decline_incoming_call" msgid="922147089348451310">"拒接"</string>
- <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"无法拨出电话,因为没有通话帐号支持拨打这类电话。"</string>
+ <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"无法拨出电话,因为没有通话账号支持拨打这类电话。"</string>
<string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string>
<string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string>
<string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"由于当前正在通过其他应用通话,因此无法拨打电话。"</string>
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"后台通话"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"通话中断"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"手机应用崩溃"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"通话流式传输"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"拨打此电话将导致<xliff:g id="OTHER_APP">%1$s</xliff:g>通话结束。"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"选择拨打此电话的方式"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"使用<xliff:g id="OTHER_APP">%1$s</xliff:g>转移呼叫"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"电信开发者菜单"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"紧急呼叫时无法接听来电。"</string>
<string name="cancel" msgid="6733466216239934756">"取消"</string>
+ <string name="back" msgid="6915955601805550206">"返回"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"手机听筒"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"蓝牙"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"有线耳机"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"免提"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"外部"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"未知"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"将音频流式传输到其他设备"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"挂断"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"在此处切换"</string>
</resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index e769d5123..0140f26a7 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"背景通話"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"已中斷的通話"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"當機的手機應用程式"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"串流通話"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"如果撥打此電話,你的 <xliff:g id="OTHER_APP">%1$s</xliff:g> 通話將會結束。"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"選擇如何撥打此電話"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"使用「<xliff:g id="OTHER_APP">%1$s</xliff:g>」將通話重新導向"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"電信開發商選單"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"使用緊急電話期間無法接聽電話。"</string>
<string name="cancel" msgid="6733466216239934756">"取消"</string>
+ <string name="back" msgid="6915955601805550206">"返回"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"聽筒"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"藍牙"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"有線耳機"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"喇叭"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"外部"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"不明"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"正在串流音訊至其他裝置"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"結束通話"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"在這裡切換"</string>
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 26a8db9d1..eeb98b56b 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"背景通話"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"通話中斷"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"通話應用程式異常終止"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"通話串流"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"撥打這通電話將結束你的「<xliff:g id="OTHER_APP">%1$s</xliff:g>」通話。"</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"選擇撥打這通電話的方式"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"使用「<xliff:g id="OTHER_APP">%1$s</xliff:g>」轉接電話"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"電信開發人員選單"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"如果裝置已撥打緊急電話,就無法進行其他通話。"</string>
<string name="cancel" msgid="6733466216239934756">"取消"</string>
+ <string name="back" msgid="6915955601805550206">"返回"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"耳機"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"藍牙"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"有線耳機"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"喇叭"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"外部"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"不明"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"正在將音訊串流到其他裝置"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"掛斷"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"切換到這部裝置"</string>
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 099173f8c..faee0d9ab 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -100,6 +100,7 @@
<string name="notification_channel_background_calls" msgid="7785659903711350506">"Amakholi angemuva"</string>
<string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Amakholi anqanyuliwe"</string>
<string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Izinhlelo zokusebenza ezikhubazekile zefoni"</string>
+ <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Ukusakaza ikholi"</string>
<string name="alert_outgoing_call" msgid="5319895109298927431">"Ukwenza le kholi kuzoqeda enye ikholi yakho ye-<xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
<string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Khetha ukuthi uyibeka kanjani le kholi"</string>
<string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Qondisa kabusha ikholi usebenzisa i-<xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
@@ -123,4 +124,14 @@
<string name="developer_title" msgid="9146088855661672353">"Imenyu yonjiniyela we-Telecom"</string>
<string name="toast_emergency_can_not_pull_call" msgid="9074229465338410869">"Amakholi awakwazi ukuthathwa ngesikhathi ukukholi yesimo esiphuthumayo."</string>
<string name="cancel" msgid="6733466216239934756">"Khansela"</string>
+ <string name="back" msgid="6915955601805550206">"Emuva"</string>
+ <string name="callendpoint_name_earpiece" msgid="7047285080319678594">"Isipikha sendlebe"</string>
+ <string name="callendpoint_name_bluetooth" msgid="210210953208913172">"I-Bluetooth"</string>
+ <string name="callendpoint_name_wiredheadset" msgid="6860787176412079742">"I-headset enentambo"</string>
+ <string name="callendpoint_name_speaker" msgid="1971760468695323189">"Isipikha"</string>
+ <string name="callendpoint_name_streaming" msgid="2337595450408275576">"Okungaphandle"</string>
+ <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Akwaziwa"</string>
+ <string name="call_streaming_notification_body" msgid="502216105683378263">"Sakaza umsindo kwenye idivayisi"</string>
+ <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Beka phansi"</string>
+ <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Shintsha lapha"</string>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bf5abca49..ec278f008 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -321,6 +321,10 @@
<string name="notification_channel_disconnected_calls">Disconnected calls</string>
<!-- Notification channel name for a channel containing crashed phone apps service notifications. -->
<string name="notification_channel_in_call_service_crash">Crashed phone apps</string>
+ <!-- Notification channel name for a channel containing notifications related to call streaming.
+ Call streaming is a feature where an app can use another device like a tablet to see and
+ control a call taking place on their phone. -->
+ <string name="notification_channel_call_streaming">Call streaming</string>
<!-- Alert dialog content used to inform the user that placing a new outgoing call will end the
ongoing call in the app "other_app". -->
@@ -381,4 +385,34 @@
<string name="developer_enhanced_call_blocking" translatable="false">Enhanced Call Blocking</string>
<!-- Button label for generic cancel action [CHAR LIMIT=20] -->
<string name="cancel">Cancel</string>
+ <!-- Button label for generic back action [CHAR LIMIT=20] -->
+ <string name="back">Back</string>
+ <!-- The user-visible name of the earpiece type CallEndpoint -->
+ <string name="callendpoint_name_earpiece">Earpiece</string>
+ <!-- The user-visible name of the bluetooth type CallEndpoint -->
+ <string name="callendpoint_name_bluetooth">Bluetooth</string>
+ <!-- The user-visible name of the wired headset type CallEndpoint -->
+ <string name="callendpoint_name_wiredheadset">Wired headset</string>
+ <!-- The user-visible name of the speaker type CallEndpoint -->
+ <string name="callendpoint_name_speaker">Speaker</string>
+ <!-- The user-visible name of the streaming type CallEndpoint -->
+ <string name="callendpoint_name_streaming">External</string>
+ <!-- The user-visible name of the unknown new type CallEndpoint -->
+ <string name="callendpoint_name_unknown">Unknown</string>
+
+ <!-- The content of a notification shown when a call is being streamed to another device.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_body">Streaming audio to other device</string>
+ <!-- A notification action which is shown when a call is being streamed to another device.
+ Tapping the action will hang up the call.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_action_hang_up">Hang up</string>
+ <!-- A notification action which is shown when a call is being streamed to another device.
+ Tapping the action will move the call back to the phone from the device it is being
+ streamed to.
+ Call streaming is a feature where a user can see and interact with a call from another
+ device like a tablet while the call takes place on their phone. -->
+ <string name="call_streaming_notification_action_switch_here">Switch here</string>
</resources>
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index 145e9b4c5..bbcf85873 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -72,96 +72,101 @@ public class Analytics {
private static final String CLEAR_ANALYTICS_ARG = "clear";
public static final Map<String, Integer> sLogEventToAnalyticsEvent = Map.ofEntries(
- entry(LogUtils.Events.SET_SELECT_PHONE_ACCOUNT,
- AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT),
- entry(LogUtils.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD),
- entry(LogUtils.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD),
- entry(LogUtils.Events.SWAP, AnalyticsEvent.SWAP),
- entry(LogUtils.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING),
- entry(LogUtils.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH),
- entry(LogUtils.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE),
- entry(LogUtils.Events.SET_PARENT, AnalyticsEvent.SET_PARENT),
- entry(LogUtils.Events.MUTE, AnalyticsEvent.MUTE),
- entry(LogUtils.Events.UNMUTE, AnalyticsEvent.UNMUTE),
- entry(LogUtils.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT),
- entry(LogUtils.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE),
- entry(LogUtils.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET),
- entry(LogUtils.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER),
- entry(LogUtils.Events.SILENCE, AnalyticsEvent.SILENCE),
- entry(LogUtils.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED),
- entry(LogUtils.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED),
- entry(LogUtils.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED),
- entry(LogUtils.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD),
- entry(LogUtils.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD),
- entry(LogUtils.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL),
- entry(LogUtils.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT),
- entry(LogUtils.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT),
- entry(LogUtils.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE),
- entry(LogUtils.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED),
- entry(LogUtils.Events.SET_HOLD, AnalyticsEvent.SET_HOLD),
- entry(LogUtils.Events.SET_DIALING, AnalyticsEvent.SET_DIALING),
- entry(LogUtils.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION),
- entry(LogUtils.Events.BIND_CS, AnalyticsEvent.BIND_CS),
- entry(LogUtils.Events.CS_BOUND, AnalyticsEvent.CS_BOUND),
- entry(LogUtils.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT),
- entry(LogUtils.Events.DIRECT_TO_VM_INITIATED,
- AnalyticsEvent.DIRECT_TO_VM_INITIATED),
- entry(LogUtils.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED),
- entry(LogUtils.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED),
- entry(LogUtils.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED),
- entry(LogUtils.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT));
+ entry(LogUtils.Events.SET_SELECT_PHONE_ACCOUNT,
+ AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT),
+ entry(LogUtils.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD),
+ entry(LogUtils.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD),
+ entry(LogUtils.Events.SWAP, AnalyticsEvent.SWAP),
+ entry(LogUtils.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING),
+ entry(LogUtils.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH),
+ entry(LogUtils.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE),
+ entry(LogUtils.Events.SET_PARENT, AnalyticsEvent.SET_PARENT),
+ entry(LogUtils.Events.MUTE, AnalyticsEvent.MUTE),
+ entry(LogUtils.Events.UNMUTE, AnalyticsEvent.UNMUTE),
+ entry(LogUtils.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT),
+ entry(LogUtils.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE),
+ entry(LogUtils.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET),
+ entry(LogUtils.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER),
+ entry(LogUtils.Events.SILENCE, AnalyticsEvent.SILENCE),
+ entry(LogUtils.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED),
+ entry(LogUtils.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED),
+ entry(LogUtils.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED),
+ entry(LogUtils.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD),
+ entry(LogUtils.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD),
+ entry(LogUtils.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL),
+ entry(LogUtils.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT),
+ entry(LogUtils.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT),
+ entry(LogUtils.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE),
+ entry(LogUtils.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED),
+ entry(LogUtils.Events.SET_HOLD, AnalyticsEvent.SET_HOLD),
+ entry(LogUtils.Events.SET_DIALING, AnalyticsEvent.SET_DIALING),
+ entry(LogUtils.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION),
+ entry(LogUtils.Events.BIND_CS, AnalyticsEvent.BIND_CS),
+ entry(LogUtils.Events.CS_BOUND, AnalyticsEvent.CS_BOUND),
+ entry(LogUtils.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT),
+ entry(LogUtils.Events.DIRECT_TO_VM_INITIATED,
+ AnalyticsEvent.DIRECT_TO_VM_INITIATED),
+ entry(LogUtils.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED),
+ entry(LogUtils.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED),
+ entry(LogUtils.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED),
+ entry(LogUtils.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT),
+ entry(LogUtils.Events.DND_PRE_CHECK_INITIATED, AnalyticsEvent.DND_CHECK_INITIATED),
+ entry(LogUtils.Events.DND_PRE_CHECK_COMPLETED, AnalyticsEvent.DND_CHECK_COMPLETED));
public static final Map<String, Integer> sLogSessionToSessionId = Map.ofEntries(
- entry(LogUtils.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL),
- entry(LogUtils.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL),
- entry(LogUtils.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL),
- entry(LogUtils.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL),
- entry(LogUtils.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL),
- entry(LogUtils.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE),
- entry(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE),
- entry(LogUtils.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE),
- entry(LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
- SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE),
- entry(LogUtils.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE),
- entry(LogUtils.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING),
- entry(LogUtils.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING),
- entry(LogUtils.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED),
- entry(LogUtils.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD),
- entry(LogUtils.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL),
- entry(LogUtils.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED),
- entry(LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
- SessionTiming.CSW_ADD_CONFERENCE_CALL));
+ entry(LogUtils.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL),
+ entry(LogUtils.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL),
+ entry(LogUtils.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL),
+ entry(LogUtils.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL),
+ entry(LogUtils.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL),
+ entry(LogUtils.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE),
+ entry(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE),
+ entry(LogUtils.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE),
+ entry(LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
+ SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE),
+ entry(LogUtils.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE),
+ entry(LogUtils.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING),
+ entry(LogUtils.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING),
+ entry(LogUtils.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED),
+ entry(LogUtils.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD),
+ entry(LogUtils.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL),
+ entry(LogUtils.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED),
+ entry(LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
+ SessionTiming.CSW_ADD_CONFERENCE_CALL));
public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming = Map.ofEntries(
- entry(LogUtils.Events.Timings.ACCEPT_TIMING,
- ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING),
- entry(LogUtils.Events.Timings.REJECT_TIMING,
- ParcelableCallAnalytics.EventTiming.REJECT_TIMING),
- entry(LogUtils.Events.Timings.DISCONNECT_TIMING,
- ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING),
- entry(LogUtils.Events.Timings.HOLD_TIMING,
- ParcelableCallAnalytics.EventTiming.HOLD_TIMING),
- entry(LogUtils.Events.Timings.UNHOLD_TIMING,
- ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING),
- entry(LogUtils.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING,
- ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING),
- entry(LogUtils.Events.Timings.BIND_CS_TIMING,
- ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING),
- entry(LogUtils.Events.Timings.SCREENING_COMPLETED_TIMING,
- ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING),
- entry(LogUtils.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING,
- ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING),
- entry(LogUtils.Events.Timings.BLOCK_CHECK_FINISHED_TIMING,
- ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING),
- entry(LogUtils.Events.Timings.FILTERING_COMPLETED_TIMING,
- ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING),
- entry(LogUtils.Events.Timings.FILTERING_TIMED_OUT_TIMING,
- ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING),
- entry(LogUtils.Events.Timings.START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING,
- ParcelableCallAnalytics.EventTiming.
- START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING));
+ entry(LogUtils.Events.Timings.ACCEPT_TIMING,
+ ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING),
+ entry(LogUtils.Events.Timings.REJECT_TIMING,
+ ParcelableCallAnalytics.EventTiming.REJECT_TIMING),
+ entry(LogUtils.Events.Timings.DISCONNECT_TIMING,
+ ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING),
+ entry(LogUtils.Events.Timings.HOLD_TIMING,
+ ParcelableCallAnalytics.EventTiming.HOLD_TIMING),
+ entry(LogUtils.Events.Timings.UNHOLD_TIMING,
+ ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING),
+ entry(LogUtils.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING,
+ ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING),
+ entry(LogUtils.Events.Timings.BIND_CS_TIMING,
+ ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING),
+ entry(LogUtils.Events.Timings.SCREENING_COMPLETED_TIMING,
+ ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING),
+ entry(LogUtils.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING,
+ ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING),
+ entry(LogUtils.Events.Timings.BLOCK_CHECK_FINISHED_TIMING,
+ ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING),
+ entry(LogUtils.Events.Timings.FILTERING_COMPLETED_TIMING,
+ ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING),
+ entry(LogUtils.Events.Timings.FILTERING_TIMED_OUT_TIMING,
+ ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING),
+ entry(LogUtils.Events.Timings.START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING,
+ ParcelableCallAnalytics.EventTiming.
+ START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING),
+ entry(LogUtils.Events.Timings.DND_PRE_CHECK_COMPLETED_TIMING,
+ ParcelableCallAnalytics.EventTiming.DND_PRE_CALL_PRE_CHECK_TIMING));
public static final Map<Integer, String> sSessionIdToLogSession = new HashMap<>();
+
static {
for (Map.Entry<String, Integer> e : sLogSessionToSessionId.entrySet()) {
sSessionIdToLogSession.put(e.getValue(), e.getKey());
@@ -228,12 +233,12 @@ public class Analytics {
public long startTime; // start time in milliseconds since the epoch. 0 if not yet set.
public long endTime; // end time in milliseconds since the epoch. 0 if not yet set.
public int callDirection; // one of UNKNOWN_DIRECTION, INCOMING_DIRECTION,
- // or OUTGOING_DIRECTION.
+ // or OUTGOING_DIRECTION.
public boolean isAdditionalCall = false; // true if the call came in while another call was
- // in progress or if the user dialed this call
- // while in the middle of another call.
+ // in progress or if the user dialed this call
+ // while in the middle of another call.
public boolean isInterrupted = false; // true if the call was interrupted by an incoming
- // or outgoing call.
+ // or outgoing call.
public int callTechnologies; // bitmask denoting which technologies a call used.
// true if the Telecom Call object was created from an existing connection via
@@ -432,17 +437,17 @@ public class Analytics {
TelecomLogClass.CallLog analyticsProto = toProto();
List<ParcelableCallAnalytics.AnalyticsEvent> events =
Arrays.stream(analyticsProto.callEvents)
- .map(callEventProto -> new ParcelableCallAnalytics.AnalyticsEvent(
- callEventProto.getEventName(),
- callEventProto.getTimeSinceLastEventMillis())
- ).collect(Collectors.toList());
+ .map(callEventProto -> new ParcelableCallAnalytics.AnalyticsEvent(
+ callEventProto.getEventName(),
+ callEventProto.getTimeSinceLastEventMillis())
+ ).collect(Collectors.toList());
List<ParcelableCallAnalytics.EventTiming> timings =
Arrays.stream(analyticsProto.callTimings)
- .map(callTimingProto -> new ParcelableCallAnalytics.EventTiming(
- callTimingProto.getTimingName(),
- callTimingProto.getTimeMillis())
- ).collect(Collectors.toList());
+ .map(callTimingProto -> new ParcelableCallAnalytics.EventTiming(
+ callTimingProto.getTimingName(),
+ callTimingProto.getTimeMillis())
+ ).collect(Collectors.toList());
ParcelableCallAnalytics result = new ParcelableCallAnalytics(
// rounds down to nearest 5 minute mark
@@ -498,7 +503,7 @@ public class Analytics {
.setConnectionProperties(callProperties)
.setCallSource(callSource);
- result.connectionService = new String[] {connectionService};
+ result.connectionService = new String[]{connectionService};
if (callEvents != null) {
result.callEvents = convertLogEventsToProtoEvents(callEvents.getEvents());
result.callTimings = callEvents.extractEventTimings().stream()
@@ -547,7 +552,6 @@ public class Analytics {
}
private String getMissedReasonString() {
- //TODO: Implement this
StringBuilder s = new StringBuilder();
s.append('[');
if ((missedReason & AUTO_MISSED_EMERGENCY_CALL) != 0) {
@@ -608,6 +612,7 @@ public class Analytics {
}
}
}
+
public static final String TAG = "TelecomAnalytics";
// Constants for call direction
@@ -842,13 +847,13 @@ public class Analytics {
}
@VisibleForTesting
- public static long roundToOneSigFig(long val) {
+ public static long roundToOneSigFig(long val) {
if (val == 0) {
return val;
}
int logVal = (int) Math.floor(Math.log10(val < 0 ? -val : val));
double s = Math.pow(10, logVal);
- double dec = val / s;
+ double dec = val / s;
return (long) (Math.round(dec) * s);
}
}
diff --git a/src/com/android/server/telecom/AnomalyReporterAdapter.java b/src/com/android/server/telecom/AnomalyReporterAdapter.java
new file mode 100644
index 000000000..7c2141981
--- /dev/null
+++ b/src/com/android/server/telecom/AnomalyReporterAdapter.java
@@ -0,0 +1,27 @@
+/*
+ * 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 java.util.UUID;
+
+/**
+ * Interface to avoid static calls to AnomalyReporter. Add methods to this interface as needed for
+ * refactoring.
+ */
+public interface AnomalyReporterAdapter {
+ void reportAnomaly(UUID eventId, String description);
+}
diff --git a/src/com/android/server/telecom/AnomalyReporterAdapterImpl.java b/src/com/android/server/telecom/AnomalyReporterAdapterImpl.java
new file mode 100644
index 000000000..c34d211d5
--- /dev/null
+++ b/src/com/android/server/telecom/AnomalyReporterAdapterImpl.java
@@ -0,0 +1,27 @@
+/*
+ * 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 android.telephony.AnomalyReporter;
+import java.util.UUID;
+
+public class AnomalyReporterAdapterImpl implements AnomalyReporterAdapter {
+ @Override
+ public void reportAnomaly(UUID eventId, String description) {
+ AnomalyReporter.reportAnomaly(eventId, description);
+ }
+}
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 7f51f1ba5..3fbac1fdb 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -18,7 +18,6 @@ package com.android.server.telecom;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.VolumeShaper;
import android.net.Uri;
@@ -27,12 +26,12 @@ import android.os.HandlerThread;
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
-import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
/**
* Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
@@ -50,12 +49,6 @@ public class AsyncRingtonePlayer {
/** The current ringtone. Only used by the ringtone thread. */
private Ringtone mRingtone;
- /**
- * CompletableFuture which signals a caller when we know whether a ringtone will play haptics
- * or not.
- */
- private CompletableFuture<Boolean> mHapticsFuture = null;
-
public AsyncRingtonePlayer() {
// Empty
}
@@ -65,35 +58,17 @@ public class AsyncRingtonePlayer {
* If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
* the volume of the ringtone as it plays.
*
- * @param factory The {@link RingtoneFactory}.
- * @param incomingCall The ringing {@link Call}.
- * @param volumeShaperConfig An optional {@link VolumeShaper.Configuration} which is applied to
- * the ringtone to change its volume while it rings.
- * @param isVibrationEnabled {@code true} if the settings and DND configuration of the device
- * is such that the vibrator should be used, {@code false} otherwise.
- * @return A {@link CompletableFuture} which on completion indicates whether or not the ringtone
- * has a haptic track. {@code True} indicates that a haptic track is present on the
- * ringtone; in this case the default vibration in {@link Ringer} should not be played.
- * {@code False} indicates that a haptic track is NOT present on the ringtone;
- * in this case the default vibration in {@link Ringer} should be trigger if needed.
+ * @param ringtoneSupplier The {@link Ringtone} factory.
+ * @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
*/
- public @NonNull
- CompletableFuture<Boolean> play(RingtoneFactory factory, Call incomingCall,
- @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean isRingerAudible,
- boolean isVibrationEnabled) {
+ public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
+ BiConsumer<Ringtone, Boolean> ringtoneConsumer) {
Log.d(this, "Posting play.");
- if (mHapticsFuture == null) {
- mHapticsFuture = new CompletableFuture<>();
- }
SomeArgs args = SomeArgs.obtain();
- args.arg1 = factory;
- args.arg2 = incomingCall;
- args.arg3 = volumeShaperConfig;
- args.arg4 = isVibrationEnabled;
- args.arg5 = isRingerAudible;
- args.arg6 = Log.createSubsession();
+ args.arg1 = ringtoneSupplier;
+ args.arg2 = ringtoneConsumer;
+ args.arg3 = Log.createSubsession();
postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
- return mHapticsFuture;
}
/** Stops playing the ringtone. */
@@ -151,83 +126,50 @@ public class AsyncRingtonePlayer {
* Starts the actual playback of the ringtone. Executes on ringtone-thread.
*/
private void handlePlay(SomeArgs args) {
- RingtoneFactory factory = (RingtoneFactory) args.arg1;
- Call incomingCall = (Call) args.arg2;
- VolumeShaper.Configuration volumeShaperConfig = (VolumeShaper.Configuration) args.arg3;
- boolean isVibrationEnabled = (boolean) args.arg4;
- boolean isRingerAudible = (boolean) args.arg5;
- Session session = (Session) args.arg6;
+ Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
+ BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
+ Session session = (Session) args.arg3;
args.recycle();
Log.continueSession(session, "ARP.hP");
try {
- // don't bother with any of this if there is an EVENT_STOP waiting.
+ // 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)) {
- completeHapticFuture(false /* ringtoneHasHaptics */);
+ ringtoneConsumer.accept(null, /* stopped= */ true);
return;
}
-
- // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected.
- // If ringer is not audible for this call, then the phone is in "Vibrate" mode.
- // Use haptic-only ringtone or do not play anything.
- if (!isRingerAudible || Uri.EMPTY.equals(incomingCall.getRingtone())) {
- if (isVibrationEnabled) {
- setRingtone(factory.getHapticOnlyRingtone());
- if (mRingtone == null) {
- completeHapticFuture(false /* ringtoneHasHaptics */);
- return;
- }
- } else {
- setRingtone(null);
- completeHapticFuture(false /* ringtoneHasHaptics */);
+ Ringtone ringtone = null;
+ boolean hasStopped = false;
+ try {
+ ringtone = ringtoneSupplier.get();
+ // Ringtone supply can be slow. Re-check for stop event.
+ if (mHandler.hasMessages(EVENT_STOP)) {
+ hasStopped = true;
+ ringtone.stop(); // proactively release the ringtone.
return;
}
- }
-
- ThreadUtil.checkNotOnMainThread();
- Log.i(this, "handlePlay: Play ringtone.");
-
- if (mRingtone == null) {
- setRingtone(factory.getRingtone(incomingCall, volumeShaperConfig));
+ // setRingtone even if null - it also stops any current ringtone to be consistent
+ // with the overall state.
+ setRingtone(ringtone);
if (mRingtone == null) {
- Uri ringtoneUri = incomingCall.getRingtone();
- String ringtoneUriString = (ringtoneUri == null) ? "null" :
- ringtoneUri.toSafeString();
- Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " +
- "factory. Skipping ringing. Uri was: " + ringtoneUriString);
- completeHapticFuture(false /* ringtoneHasHaptics */);
+ // The ringtoneConsumer can still vibrate at this stage.
+ Log.w(this, "No ringtone was found bail out from playing.");
return;
}
- }
-
- // With the ringtone to play now known, we can determine if it has haptic channels or
- // not; we will complete the haptics future so the default vibration code in Ringer can
- // know whether to trigger the vibrator.
- if (mHapticsFuture != null && !mHapticsFuture.isDone()) {
- boolean hasHaptics = factory.hasHapticChannels(mRingtone);
- Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics,
- isVibrationEnabled);
- SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
- if (hasHaptics && (volumeShaperConfig == null
- || systemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled())) {
- AudioAttributes attributes = mRingtone.getAudioAttributes();
- Log.d(this, "handlePlay: %s haptic channel",
- (isVibrationEnabled ? "unmuting" : "muting"));
- mRingtone.setAudioAttributes(
- new AudioAttributes.Builder(attributes)
- .setHapticChannelsMuted(!isVibrationEnabled)
- .build());
+ Uri uri = mRingtone.getUri();
+ String uriString = (uri != null ? uri.toSafeString() : "");
+ Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
+ mRingtone.setLooping(true);
+ if (mRingtone.isPlaying()) {
+ Log.d(this, "Ringtone already playing.");
+ return;
}
- completeHapticFuture(hasHaptics);
+ mRingtone.play();
+ Log.i(this, "Play ringtone, looping.");
+ } finally {
+ ringtoneConsumer.accept(ringtone, hasStopped);
}
-
- mRingtone.setLooping(true);
- if (mRingtone.isPlaying()) {
- Log.d(this, "Ringtone already playing.");
- return;
- }
- mRingtone.play();
- Log.i(this, "Play ringtone, looping.");
} finally {
Log.cancelSubsession(session);
}
@@ -268,11 +210,4 @@ public class AsyncRingtonePlayer {
}
mRingtone = ringtone;
}
-
- private void completeHapticFuture(boolean ringtoneHasHaptics) {
- if (mHapticsFuture != null) {
- mHapticsFuture.complete(ringtoneHasHaptics);
- mHapticsFuture = null;
- }
- }
}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 60016fd7b..dd8e7e8a5 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -17,6 +17,7 @@
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 android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +39,7 @@ import android.os.UserHandle;
import android.provider.CallLog;
import android.provider.ContactsContract.Contacts;
import android.telecom.BluetoothCallQualityReport;
+import android.telecom.CallAttributes;
import android.telecom.CallAudioState;
import android.telecom.CallDiagnosticService;
import android.telecom.CallDiagnostics;
@@ -68,6 +70,8 @@ import android.widget.Toast;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IVideoProvider;
import com.android.internal.util.Preconditions;
+import com.android.server.telecom.stats.CallFailureCause;
+import com.android.server.telecom.stats.CallStateChangedAtomWriter;
import com.android.server.telecom.ui.ToastFactory;
import java.io.IOException;
@@ -92,7 +96,6 @@ import java.util.stream.Collectors;
* from the time the call intent was received by Telecom (vs. the time the call was
* connected etc).
*/
-@VisibleForTesting
public class Call implements CreateConnectionResponse, EventManager.Loggable,
ConnectionServiceFocusManager.CallFocus {
public final static String CALL_ID_UNKNOWN = "-1";
@@ -118,54 +121,60 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
/**
* Listener for events on the call.
*/
- @VisibleForTesting
public interface Listener {
- void onSuccessfulOutgoingCall(Call call, int callState);
- void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause);
- void onSuccessfulIncomingCall(Call call);
- void onFailedIncomingCall(Call call);
- void onSuccessfulUnknownCall(Call call, int callState);
- void onFailedUnknownCall(Call call);
- void onRingbackRequested(Call call, boolean ringbackRequested);
- void onPostDialWait(Call call, String remaining);
- void onPostDialChar(Call call, char nextChar);
- void onConnectionCapabilitiesChanged(Call call);
- void onConnectionPropertiesChanged(Call call, boolean didRttChange);
- void onParentChanged(Call call);
- void onChildrenChanged(Call call);
- void onCannedSmsResponsesLoaded(Call call);
- void onVideoCallProviderChanged(Call call);
- void onCallerInfoChanged(Call call);
- void onIsVoipAudioModeChanged(Call call);
- void onStatusHintsChanged(Call call);
- void onExtrasChanged(Call c, int source, Bundle extras);
- void onExtrasRemoved(Call c, int source, List<String> keys);
- void onHandleChanged(Call call);
- void onCallerDisplayNameChanged(Call call);
- void onCallDirectionChanged(Call call);
- void onVideoStateChanged(Call call, int previousVideoState, int newVideoState);
- void onTargetPhoneAccountChanged(Call call);
- void onConnectionManagerPhoneAccountChanged(Call call);
- void onPhoneAccountChanged(Call call);
- void onConferenceableCallsChanged(Call call);
- void onConferenceStateChanged(Call call, boolean isConference);
- void onCdmaConferenceSwap(Call call);
- boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout);
- void onHoldToneRequested(Call call);
- void onCallHoldFailed(Call call);
- void onCallSwitchFailed(Call call);
- void onConnectionEvent(Call call, String event, Bundle extras);
- void onExternalCallChanged(Call call, boolean isExternalCall);
- void onRttInitiationFailure(Call call, int reason);
- void onRemoteRttRequest(Call call, int requestId);
- void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
- Bundle extras, boolean isLegacy);
- void onHandoverFailed(Call call, int error);
- void onHandoverComplete(Call call);
- void onBluetoothCallQualityReport(Call call, BluetoothCallQualityReport report);
- void onReceivedDeviceToDeviceMessage(Call call, int messageType, int messageValue);
- void onReceivedCallQualityReport(Call call, CallQuality callQuality);
- void onCallerNumberVerificationStatusChanged(Call call, int callerNumberVerificationStatus);
+ default void onSuccessfulOutgoingCall(Call call, int callState) {};
+ default void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {};
+ default void onSuccessfulIncomingCall(Call call) {};
+ default void onFailedIncomingCall(Call call) {};
+ default void onSuccessfulUnknownCall(Call call, int callState) {};
+ default void onFailedUnknownCall(Call call) {};
+ default void onRingbackRequested(Call call, boolean ringbackRequested) {};
+ default void onPostDialWait(Call call, String remaining) {};
+ default void onPostDialChar(Call call, char nextChar) {};
+ default void onConnectionCapabilitiesChanged(Call call) {};
+ default void onConnectionPropertiesChanged(Call call, boolean didRttChange) {};
+ default void onParentChanged(Call call) {};
+ default void onChildrenChanged(Call call) {};
+ default void onCannedSmsResponsesLoaded(Call call) {};
+ default void onVideoCallProviderChanged(Call call) {};
+ default void onCallerInfoChanged(Call call) {};
+ default void onIsVoipAudioModeChanged(Call call) {};
+ default void onStatusHintsChanged(Call call) {};
+ default void onExtrasChanged(Call c, int source, Bundle extras,
+ String requestingPackageName) {};
+ default void onExtrasRemoved(Call c, int source, List<String> keys) {};
+ default void onHandleChanged(Call call) {};
+ default void onCallerDisplayNameChanged(Call call) {};
+ default void onCallDirectionChanged(Call call) {};
+ default void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {};
+ default void onTargetPhoneAccountChanged(Call call) {};
+ default void onConnectionManagerPhoneAccountChanged(Call call) {};
+ default void onPhoneAccountChanged(Call call) {};
+ default void onConferenceableCallsChanged(Call call) {};
+ default void onConferenceStateChanged(Call call, boolean isConference) {};
+ default void onCdmaConferenceSwap(Call call) {};
+ default boolean onCanceledViaNewOutgoingCallBroadcast(Call call,
+ long disconnectionTimeout) {
+ return false;
+ };
+ default void onHoldToneRequested(Call call) {};
+ default void onCallHoldFailed(Call call) {};
+ default void onCallSwitchFailed(Call call) {};
+ default void onConnectionEvent(Call call, String event, Bundle extras) {};
+ default void onCallStreamingStateChanged(Call call, boolean isStreaming) {}
+ default void onExternalCallChanged(Call call, boolean isExternalCall) {};
+ default void onRttInitiationFailure(Call call, int reason) {};
+ default void onRemoteRttRequest(Call call, int requestId) {};
+ default void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
+ Bundle extras, boolean isLegacy) {};
+ default void onHandoverFailed(Call call, int error) {};
+ default void onHandoverComplete(Call call) {};
+ default void onBluetoothCallQualityReport(Call call, BluetoothCallQualityReport report) {};
+ default void onReceivedDeviceToDeviceMessage(Call call, int messageType,
+ int messageValue) {};
+ default void onReceivedCallQualityReport(Call call, CallQuality callQuality) {};
+ default void onCallerNumberVerificationStatusChanged(Call call,
+ int callerNumberVerificationStatus) {};
}
public abstract static class ListenerBase implements Listener {
@@ -206,7 +215,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@Override
public void onStatusHintsChanged(Call call) {}
@Override
- public void onExtrasChanged(Call c, int source, Bundle extras) {}
+ public void onExtrasChanged(Call c, int source, Bundle extras,
+ String requestingPackageName) {}
@Override
public void onExtrasRemoved(Call c, int source, List<String> keys) {}
@Override
@@ -242,6 +252,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@Override
public void onConnectionEvent(Call call, String event, Bundle extras) {}
@Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {}
+ @Override
public void onExternalCallChanged(Call call, boolean isExternalCall) {}
@Override
public void onRttInitiationFailure(Call call, int reason) {}
@@ -306,6 +318,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
*/
private long mCreationTimeMillis;
+ /**
+ * The elapsed realtime millis when this call was created; this can be used to determine how
+ * long has elapsed since the call was first created.
+ */
+ private long mCreationElapsedRealtimeMillis;
+
/** The time this call was made active. */
private long mConnectTimeMillis = 0;
@@ -344,7 +362,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
private PhoneAccountHandle mRemotePhoneAccountHandle;
- private UserHandle mInitiatingUser;
+ private UserHandle mAssociatedUser;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -353,6 +371,17 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
/** The state of the call. */
private int mState;
+ /**
+ * Determines whether the {@link ConnectionService} has responded to the initial request to
+ * create the connection.
+ *
+ * {@code false} indicates the {@link Call} has been added to Telecom, but the
+ * {@link Connection} has not yet been returned by the associated {@link ConnectionService}.
+ * {@code true} indicates the {@link Call} has an associated {@link Connection} reported by the
+ * {@link ConnectionService}.
+ */
+ private boolean mIsCreateConnectionComplete = false;
+
/** The handle with which to establish this call. */
private Uri mHandle;
@@ -381,8 +410,20 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
*/
private ConnectionServiceWrapper mConnectionService;
+ private TransactionalServiceWrapper mTransactionalService;
+
private boolean mIsEmergencyCall;
+ /**
+ * Flag indicating if ECBM is active for the target phone account. This only applies to MT calls
+ * in the scenario of work profiles (when the profile is paused and the user has only registered
+ * a work sim). Normally, MT calls made to the work sim should be rejected when the work apps
+ * are paused. However, when the admin makes a MO ecall, ECBM should be enabled for that sim to
+ * allow non-emergency MT calls. MO calls don't apply because the phone account would be
+ * rejected from selection if the owner is not placing the call.
+ */
+ private boolean mIsInECBM;
+
// The Call is considered an emergency call for testing, but will not actually connect to
// emergency services.
private boolean mIsTestEmergencyCall;
@@ -485,12 +526,15 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
private final String mId;
private String mConnectionId;
private Analytics.CallInfo mAnalytics = new Analytics.CallInfo();
+ private CallStateChangedAtomWriter mCallStateChangedAtomWriter =
+ new CallStateChangedAtomWriter();
private char mPlayingDtmfTone;
private boolean mWasConferencePreviouslyMerged = false;
private boolean mWasHighDefAudio = false;
private boolean mWasWifi = false;
private boolean mWasVolte = false;
+ private boolean mDestroyed = false;
// For conferences which support merge/swap at their level, we retain a notion of an active
// call. This is used for BluetoothPhoneService. In order to support hold/merge, it must have
@@ -529,6 +573,32 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
*/
private boolean mIsSelfManaged = false;
+ private boolean mIsTransactionalCall = false;
+ private CallingPackageIdentity mCallingPackageIdentity = new CallingPackageIdentity();
+
+ /**
+ * CallingPackageIdentity is responsible for storing properties about the calling package that
+ * initiated the call. For example, if MyVoipApp requests to add a call with Telecom, we can
+ * store their UID and PID when we are still bound to that package.
+ */
+ public static class CallingPackageIdentity {
+ public int mCallingPackageUid = -1;
+ public int mCallingPackagePid = -1;
+
+ public CallingPackageIdentity() {
+ }
+
+ CallingPackageIdentity(Bundle extras) {
+ mCallingPackageUid = extras.getInt(CallAttributes.CALLER_UID_KEY, -1);
+ mCallingPackagePid = extras.getInt(CallAttributes.CALLER_PID_KEY, -1);
+ }
+ }
+
+ /**
+ * Indicates whether this call is streaming.
+ */
+ private boolean mIsStreaming = false;
+
/**
* Indicates whether the {@link PhoneAccount} associated with an self-managed call want to
* expose the call to an {@link android.telecom.InCallService} which declares the metadata
@@ -760,8 +830,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
mGatewayInfo = gatewayInfo;
setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
- setTargetPhoneAccount(targetPhoneAccountHandle);
mCallDirection = callDirection;
+ setTargetPhoneAccount(targetPhoneAccountHandle);
mIsConference = isConference;
mShouldAttachToExistingConnection = shouldAttachToExistingConnection
|| callDirection == CALL_DIRECTION_INCOMING;
@@ -769,8 +839,11 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
mClockProxy = clockProxy;
mToastFactory = toastFactory;
mCreationTimeMillis = mClockProxy.currentTimeMillis();
+ mCreationElapsedRealtimeMillis = mClockProxy.elapsedRealtime();
mMissedReason = MISSED_REASON_NOT_MISSED;
mStartRingTime = 0;
+
+ mCallStateChangedAtomWriter.setExistingCallCount(callsManager.getCalls().size());
}
/**
@@ -857,6 +930,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
public void destroy() {
+ if (mDestroyed) {
+ return;
+ }
// We should not keep these bitmaps around because the Call objects may be held for logging
// purposes.
// TODO: Make a container object that only stores the information we care about for Logging.
@@ -867,6 +943,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
closeRttStreams();
Log.addEvent(this, LogUtils.Events.DESTROYED);
+ mDestroyed = true;
}
private void closeRttStreams() {
@@ -927,6 +1004,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
s.append(SimpleDateFormat.getDateTimeInstance().format(new Date(getCreationTimeMillis())));
s.append("]");
s.append(isIncoming() ? "(MT - incoming)" : "(MO - outgoing)");
+ s.append("(User=");
+ s.append(getAssociatedUser());
+ s.append(")");
s.append("\n\t");
PhoneAccountHandle targetPhoneAccountHandle = getTargetPhoneAccount();
@@ -1038,10 +1118,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@Override
public ConnectionServiceFocusManager.ConnectionServiceFocus getConnectionServiceWrapper() {
- return mConnectionService;
+ return (!mIsTransactionalCall ? mConnectionService : mTransactionalService);
}
- @VisibleForTesting
public int getState() {
return mState;
}
@@ -1248,10 +1327,15 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
Log.addEvent(this, event, stringData);
}
- int statsdDisconnectCause = (newState == CallState.DISCONNECTED) ?
- getDisconnectCause().getCode() : DisconnectCause.UNKNOWN;
- TelecomStatsLog.write(TelecomStatsLog.CALL_STATE_CHANGED, newState,
- statsdDisconnectCause, isSelfManaged(), isExternalCall());
+
+ mCallStateChangedAtomWriter
+ .setDisconnectCause(getDisconnectCause())
+ .setSelfManaged(isSelfManaged())
+ .setExternalCall(isExternalCall())
+ .setEmergencyCall(isEmergencyCall())
+ .setDurationSeconds(Long.valueOf(
+ (mDisconnectTimeMillis - mConnectTimeMillis) / 1000).intValue())
+ .write(newState);
}
return true;
}
@@ -1272,13 +1356,34 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
Bundle bundle = new Bundle();
bundle.putBoolean(android.telecom.Call.EXTRA_SILENT_RINGING_REQUESTED,
silentRingingRequested);
- putExtras(SOURCE_CONNECTION_SERVICE, bundle);
+ putConnectionServiceExtras(bundle);
}
public boolean isSilentRingingRequested() {
return mSilentRingingRequested;
}
+ public void setCallIsSuppressedByDoNotDisturb(boolean isCallSuppressed) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB,
+ isCallSuppressed);
+ putConnectionServiceExtras(bundle);
+ }
+
+ public boolean isCallSuppressedByDoNotDisturb() {
+ if (getExtras() == null) {
+ return false;
+ }
+ return getExtras().getBoolean(android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB);
+ }
+
+ public boolean wasDndCheckComputedForCall() {
+ if (getExtras() == null) {
+ return false;
+ }
+ return getExtras().containsKey(android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB);
+ }
+
@VisibleForTesting
public boolean isConference() {
return mIsConference;
@@ -1401,6 +1506,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
}
+ public Uri getContactPhotoUri() {
+ return mCallerInfo != null ? mCallerInfo.getContactDisplayPhotoUri() : null;
+ }
+
public String getCallerDisplayName() {
return mCallerDisplayName;
}
@@ -1420,6 +1529,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
}
+ void setContactPhotoUri(Uri contactPhotoUri) {
+ if (mCallerInfo != null) {
+ mCallerInfo.SetContactDisplayPhotoUri(contactPhotoUri);
+ }
+ }
+
public String getName() {
return mCallerInfo == null ? null : mCallerInfo.getName();
}
@@ -1472,12 +1587,20 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
* @return {@code true} if this is an outgoing call to emergency services. An outgoing call is
* identified as an emergency call by the dialer phone number.
*/
- @VisibleForTesting
public boolean isEmergencyCall() {
return mIsEmergencyCall;
}
/**
+ * For testing purposes, set if this call is an emergency call or not.
+ * @param isEmergencyCall {@code true} if emergency, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ public void setIsEmergencyCall(boolean isEmergencyCall) {
+ mIsEmergencyCall = isEmergencyCall;
+ }
+
+ /**
* @return {@code true} if this an outgoing call to a test emergency number (and NOT to
* emergency services). Used for testing purposes to differentiate between a real and fake
* emergency call for safety reasons during testing.
@@ -1487,6 +1610,21 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
/**
+ * @return {@code true} if the target phone account is in ECBM.
+ */
+ public boolean isInECBM() {
+ return mIsInECBM;
+ }
+
+ /**
+ * Set if the target phone account is in ECBM.
+ * @param isInEcbm {@code true} if target phone account is in ECBM, {@code false} otherwise.
+ */
+ public void setIsInECBM(boolean isInECBM) {
+ mIsInECBM = isInECBM;
+ }
+
+ /**
* @return {@code true} if the network has identified this call as an emergency call.
*/
public boolean isNetworkIdentifiedEmergencyCall() {
@@ -1577,6 +1715,11 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
public void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
mTargetPhoneAccountHandle = accountHandle;
+ // Update the last MO emergency call in the helper, if applicable.
+ if (isEmergencyCall() && !isIncoming()) {
+ mCallsManager.getEmergencyCallHelper().setLastOutgoingEmergencyCallPAH(
+ accountHandle);
+ }
for (Listener l : mListeners) {
l.onTargetPhoneAccountChanged(this);
}
@@ -1584,6 +1727,30 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
checkIfVideoCapable();
checkIfRttCapable();
+
+ if (accountHandle != null) {
+ mCallStateChangedAtomWriter.setUid(
+ 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());
+ }
+ }
+ }
+
+ public PhoneAccount getPhoneAccountFromHandle() {
+ if (getTargetPhoneAccount() == null) {
+ return null;
+ }
+ PhoneAccount phoneAccount = mCallsManager.getPhoneAccountRegistrar()
+ .getPhoneAccountUnchecked(getTargetPhoneAccount());
+
+ if (phoneAccount == null) {
+ return null;
+ }
+
+ return phoneAccount;
}
public CharSequence getTargetPhoneAccountLabel() {
@@ -1716,6 +1883,36 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
setConnectionProperties(getConnectionProperties());
}
+ public boolean isTransactionalCall() {
+ return mIsTransactionalCall;
+ }
+
+ public void setIsTransactionalCall(boolean isTransactionalCall) {
+ mIsTransactionalCall = isTransactionalCall;
+
+ // Connection properties will add/remove the PROPERTY_SELF_MANAGED.
+ setConnectionProperties(getConnectionProperties());
+ }
+
+ public void setCallingPackageIdentity(Bundle extras) {
+ mCallingPackageIdentity = new CallingPackageIdentity(extras);
+ // These extras should NOT be propagated to Dialer and should be removed.
+ extras.remove(CallAttributes.CALLER_PID_KEY);
+ extras.remove(CallAttributes.CALLER_UID_KEY);
+ }
+
+ public CallingPackageIdentity getCallingPackageIdentity() {
+ return mCallingPackageIdentity;
+ }
+
+ public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
+ mTransactionalService = service;
+ }
+
+ public TransactionalServiceWrapper getTransactionServiceWrapper() {
+ return mTransactionalService;
+ }
+
public boolean visibleToInCallService() {
return mVisibleToInCallService;
}
@@ -1786,7 +1983,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
if (phoneAccount != null) {
final UserHandle userHandle;
if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
- userHandle = mInitiatingUser;
+ userHandle = mAssociatedUser;
} else {
userHandle = mTargetPhoneAccountHandle.getUserHandle();
}
@@ -1897,8 +2094,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
return mCreationTimeMillis;
}
- public void setCreationTimeMillis(long time) {
- mCreationTimeMillis = time;
+ /**
+ * @return The elapsed realtime millis when the call was created; ONLY useful for determining
+ * how long has elapsed since the call was first created.
+ */
+ public long getCreationElapsedRealtimeMillis() {
+ return mCreationElapsedRealtimeMillis;
}
public long getConnectTimeMillis() {
@@ -1993,8 +2194,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
createRttStreams();
// Call startRtt to pass the RTT pipes down to the connection service.
// They already turned on the RTT property so no request should be sent.
- mConnectionService.startRtt(this,
- getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
+ if (mConnectionService != null) {
+ mConnectionService.startRtt(this,
+ getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
+ }
mWasEverRtt = true;
if (isEmergencyCall()) {
mCallsManager.mute(false);
@@ -2095,7 +2298,6 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
return mConferenceLevelActiveCall;
}
- @VisibleForTesting
public ConnectionServiceWrapper getConnectionService() {
return mConnectionService;
}
@@ -2192,6 +2394,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
CallIdMapper idMapper,
ParcelableConference conference) {
Log.v(this, "handleCreateConferenceSuccessful %s", conference);
+ mIsCreateConnectionComplete = true;
setTargetPhoneAccount(conference.getPhoneAccount());
setHandle(conference.getHandle(), conference.getHandlePresentation());
@@ -2201,7 +2404,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
setVideoState(conference.getVideoState());
setRingbackRequested(conference.isRingbackRequested());
setStatusHints(conference.getStatusHints());
- putExtras(SOURCE_CONNECTION_SERVICE, conference.getExtras());
+ putConnectionServiceExtras(conference.getExtras());
switch (mCallDirection) {
case CALL_DIRECTION_INCOMING:
@@ -2225,11 +2428,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
CallIdMapper idMapper,
ParcelableConnection connection) {
Log.v(this, "handleCreateConnectionSuccessful %s", connection);
+ mIsCreateConnectionComplete = true;
setTargetPhoneAccount(connection.getPhoneAccount());
setHandle(connection.getHandle(), connection.getHandlePresentation());
+
setCallerDisplayName(
connection.getCallerDisplayName(), connection.getCallerDisplayNamePresentation());
-
setConnectionCapabilities(connection.getConnectionCapabilities());
setConnectionProperties(connection.getConnectionProperties());
setIsVoipAudioMode(connection.getIsVoipAudioMode());
@@ -2238,7 +2442,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
setVideoState(connection.getVideoState());
setRingbackRequested(connection.isRingbackRequested());
setStatusHints(connection.getStatusHints());
- putExtras(SOURCE_CONNECTION_SERVICE, connection.getExtras());
+ putConnectionServiceExtras(connection.getExtras());
mConferenceableCalls.clear();
for (String id : connection.getConferenceableConnectionIds()) {
@@ -2272,6 +2476,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@Override
public void handleCreateConferenceFailure(DisconnectCause disconnectCause) {
+ Log.i(this, "handleCreateConferenceFailure; callid=%s, disconnectCause=%s",
+ getId(), disconnectCause);
clearConnectionService();
setDisconnectCause(disconnectCause);
mCallsManager.markCallAsDisconnected(this, disconnectCause);
@@ -2292,6 +2498,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@Override
public void handleCreateConnectionFailure(DisconnectCause disconnectCause) {
+ Log.i(this, "handleCreateConnectionFailure; callid=%s, disconnectCause=%s",
+ getId(), disconnectCause);
clearConnectionService();
setDisconnectCause(disconnectCause);
mCallsManager.markCallAsDisconnected(this, disconnectCause);
@@ -2371,7 +2579,6 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
disconnect(0);
}
- @VisibleForTesting
public void disconnect(String reason) {
disconnect(0, reason);
}
@@ -2400,7 +2607,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT ||
mState == CallState.CONNECTING) {
- Log.v(this, "Aborting call %s", this);
+ Log.i(this, "disconnect: Aborting call %s", getId());
abort(disconnectionTimeout);
} else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) {
@@ -2411,7 +2618,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// Override the disconnect cause to MISSED
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
}
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onDisconnect(this, getDisconnectCause());
+ Log.i(this, "Send Disconnect to transactional service for call");
+ } else if (mConnectionService == null) {
Log.e(this, new Exception(), "disconnect() request on a call without a"
+ " connection service.");
} else {
@@ -2480,6 +2690,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// {@link ConnectionServiceAdapter#setActive} and other set* methods.
if (mConnectionService != null) {
mConnectionService.answer(this, videoState);
+ } else if (mTransactionalService != null) {
+ mTransactionalService.onAnswer(this, videoState);
} else {
Log.e(this, new NullPointerException(),
"answer call failed due to null CS callId=%s", getId());
@@ -2571,7 +2783,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// ringing. Since the call is already active on the connectionservice side, we want to
// hangup, not reject.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onDisconnect(this,
+ new DisconnectCause(DisconnectCause.REJECTED));
+ } else if (mConnectionService != null) {
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2582,7 +2797,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onDisconnect(this,
+ new DisconnectCause(DisconnectCause.REJECTED));
+ } else if (mConnectionService != null) {
mConnectionService.reject(this, rejectWithMessage, textMessage);
} else {
Log.e(this, new NullPointerException(),
@@ -2603,7 +2821,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// hangup, not reject.
// Since its simulated reason we can't pass along the reject reason.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onDisconnect(this,
+ new DisconnectCause(DisconnectCause.REJECTED));
+ } else if (mConnectionService != null) {
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2613,8 +2834,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
} else if (isRinging("reject") || isAnswered("reject")) {
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
-
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onDisconnect(this,
+ new DisconnectCause(DisconnectCause.REJECTED));
+ } else if (mConnectionService != null) {
mConnectionService.rejectWithReason(this, rejectReason);
} else {
Log.e(this, new NullPointerException(),
@@ -2633,7 +2856,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@VisibleForTesting
public void transfer(Uri number, boolean isConfirmationRequired) {
if (mState == CallState.ACTIVE || mState == CallState.ON_HOLD) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "transfer: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.transfer(this, number, isConfirmationRequired);
} else {
Log.e(this, new NullPointerException(),
@@ -2653,7 +2878,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
public void transfer(Call otherCall) {
if (mState == CallState.ACTIVE &&
(otherCall != null && otherCall.getState() == CallState.ON_HOLD)) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "transfer: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.transfer(this, otherCall);
} else {
Log.e(this, new NullPointerException(),
@@ -2673,7 +2900,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
public void hold(String reason) {
if (mState == CallState.ACTIVE) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onSetInactive(this);
+ } else if (mConnectionService != null) {
mConnectionService.hold(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2693,7 +2922,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
public void unhold(String reason) {
if (mState == CallState.ON_HOLD) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null){
+ mTransactionalService.onSetActive(this);
+ } else if (mConnectionService != null){
mConnectionService.unhold(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2718,7 +2949,6 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
}
- @VisibleForTesting
public boolean isActive() {
return mState == CallState.ACTIVE;
}
@@ -2729,18 +2959,45 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
/**
+ * Adds extras to the extras bundle associated with this {@link Call}, as made by a
+ * {@link ConnectionService} or other non {@link android.telecom.InCallService} source.
+ *
+ * @param extras The extras.
+ */
+ public void putConnectionServiceExtras(Bundle extras) {
+ putExtras(SOURCE_CONNECTION_SERVICE, extras, null);
+ }
+
+ /**
+ * Adds extras to the extras bundle associated with this {@link Call}, as made by a
+ * {@link android.telecom.InCallService}.
+ * @param extras the extras.
+ * @param requestingPackageName the package name of the {@link android.telecom.InCallService}
+ * which requested the extras changed; required so that when we
+ * have {@link InCallController} notify other
+ * {@link android.telecom.InCallService}s we don't notify the
+ * originator of their own change.
+ */
+ public void putInCallServiceExtras(Bundle extras, String requestingPackageName) {
+ putExtras(SOURCE_INCALL_SERVICE, extras, requestingPackageName);
+ }
+
+ /**
* Adds extras to the extras bundle associated with this {@link Call}.
*
* Note: this method needs to know the source of the extras change (see
* {@link #SOURCE_CONNECTION_SERVICE}, {@link #SOURCE_INCALL_SERVICE}). Extras changes which
- * originate from a connection service will only be notified to incall services. Likewise,
- * changes originating from the incall services will only notify the connection service of the
- * change.
+ * originate from a connection service will only be notified to incall services. Changes
+ * originating from the InCallServices will notify the connection service of the
+ * change, as well as other InCallServices other than the originator.
*
* @param source The source of the extras addition.
* @param extras The extras.
+ * @param requestingPackageName The package name which requested the extras change. For
+ * {@link #SOURCE_INCALL_SERVICE} will be populated with the
+ * package name of the ICS that requested the change.
*/
- public void putExtras(int source, Bundle extras) {
+ private void putExtras(int source, Bundle extras, String requestingPackageName) {
if (extras == null) {
return;
}
@@ -2750,7 +3007,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
mExtras.putAll(extras);
for (Listener l : mListeners) {
- l.onExtrasChanged(this, source, extras);
+ l.onExtrasChanged(this, source, extras, requestingPackageName);
}
// If mExtra shows that the call using Volte, record it with mWasVolte
@@ -2784,7 +3041,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
- if (mConnectionService != null) {
+ Log.addEvent(this, LogUtils.Events.ICS_EXTRAS_CHANGED);
+ if (mTransactionalService != null) {
+ Log.i(this, "putExtras: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
Log.e(this, new NullPointerException(),
@@ -2819,7 +3079,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "removeExtras: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
Log.e(this, new NullPointerException(),
@@ -2873,7 +3135,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
void postDialContinue(boolean proceed) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "postDialContinue: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.onPostDialContinue(this, proceed);
} else {
Log.e(this, new NullPointerException(),
@@ -2882,7 +3146,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
void conferenceWith(Call otherCall) {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "conferenceWith: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "conference requested on a call without a connection service.");
} else {
Log.addEvent(this, LogUtils.Events.CONFERENCE_WITH, otherCall);
@@ -2891,7 +3157,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
void splitFromConference() {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "splitFromConference: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "splitting from conference call without a connection service");
} else {
Log.addEvent(this, LogUtils.Events.SPLIT_FROM_CONFERENCE);
@@ -2901,7 +3169,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@VisibleForTesting
public void mergeConference() {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "mergeConference: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "merging conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
Log.addEvent(this, LogUtils.Events.CONFERENCE_WITH);
@@ -2912,7 +3182,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
@VisibleForTesting
public void swapConference() {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "swapConference: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "swapping conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
Log.addEvent(this, LogUtils.Events.SWAP);
@@ -2938,7 +3210,9 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
public void addConferenceParticipants(List<Uri> participants) {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "addConferenceParticipants: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "adding conference participants without a connection service.");
} else if (can(Connection.CAPABILITY_ADD_PARTICIPANT)) {
Log.addEvent(this, LogUtils.Events.ADD_PARTICIPANT);
@@ -2966,6 +3240,11 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
* If there is an ongoing emergency call, pull requests are also ignored.
*/
public void pullExternalCall() {
+ if (mTransactionalService != null) {
+ Log.i(this, "transfer: called on TransactionalService. doing nothing");
+ return;
+ }
+
if (mConnectionService == null) {
Log.w(this, "pulling a call without a connection service.");
}
@@ -3013,7 +3292,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
* @param extras Associated extras.
*/
public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
- if (mConnectionService != null) {
+ 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" +
@@ -3036,8 +3315,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
if (extras == null) {
Log.w(this, "sendCallEvent: %s event received with null extras.",
android.telecom.Call.EVENT_REQUEST_HANDOVER);
- mConnectionService.sendCallEvent(this,
- android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+ sendEventToService(this, android.telecom.Call.EVENT_HANDOVER_FAILED,
+ null);
return;
}
Parcelable parcelable = extras.getParcelable(
@@ -3045,8 +3324,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
if (!(parcelable instanceof PhoneAccountHandle) || parcelable == null) {
Log.w(this, "sendCallEvent: %s event received with invalid handover acct.",
android.telecom.Call.EVENT_REQUEST_HANDOVER);
- mConnectionService.sendCallEvent(this,
- android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+ sendEventToService(this, android.telecom.Call.EVENT_HANDOVER_FAILED, null);
return;
}
PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) parcelable;
@@ -3069,7 +3347,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
));
}
Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
- mConnectionService.sendCallEvent(this, event, extras);
+ sendEventToService(this, event, extras);
}
} else {
Log.e(this, new NullPointerException(),
@@ -3078,6 +3356,17 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
/**
+ * This method should only be called from sendCallEvent(String, int, 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);
+ }
+ }
+
+ /**
* Notifies listeners when a bluetooth quality report is received.
* @param report The bluetooth quality report.
*/
@@ -3373,11 +3662,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
return;
}
+ String newName = callerInfo.getName();
+ boolean contactNameChanged = mCallerInfo == null || !mCallerInfo.getName().equals(newName);
+
mCallerInfo = callerInfo;
Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
- if (mCallerInfo.getContactDisplayPhotoUri() == null ||
- mCallerInfo.cachedPhotoIcon != null || mCallerInfo.cachedPhoto != null) {
+ if (mCallerInfo.getContactDisplayPhotoUri() == null || mCallerInfo.cachedPhotoIcon != null
+ || mCallerInfo.cachedPhoto != null || contactNameChanged) {
for (Listener l : mListeners) {
l.onCallerInfoChanged(this);
}
@@ -3443,7 +3735,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
public void stopRtt() {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "stopRtt: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
+ Log.addEvent(this, LogUtils.Events.REQUEST_RTT, "stop");
mConnectionService.stopRtt(this);
} else {
// If this gets called by the in-call app before the connection service is set, we'll
@@ -3453,6 +3748,11 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
public void sendRttRequest() {
+ if (mTransactionalService != null) {
+ Log.i(this, "sendRttRequest: called on TransactionalService. doing nothing");
+ return;
+ }
+ Log.addEvent(this, LogUtils.Events.REQUEST_RTT, "start");
createRttStreams();
mConnectionService.startRtt(this, getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
}
@@ -3476,12 +3776,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
public void onRttConnectionFailure(int reason) {
Log.i(this, "Got RTT initiation failure with reason %d", reason);
+ Log.addEvent(this, LogUtils.Events.ON_RTT_FAILED, "reason=" + reason);
for (Listener l : mListeners) {
l.onRttInitiationFailure(this, reason);
}
}
public void onRemoteRttRequest() {
+ Log.addEvent(this, LogUtils.Events.ON_RTT_REQUEST);
if (isRttCall()) {
Log.w(this, "Remote RTT request on a call that's already RTT");
return;
@@ -3502,6 +3804,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
Log.w(this, "Response ID %d does not match expected %d", id, mPendingRttRequestId);
return;
}
+ if (mTransactionalService != null) {
+ Log.i(this, "handleRttRequestResponse: called on TransactionalService. doing nothing");
+ return;
+ }
+ Log.addEvent(this, LogUtils.Events.RESPOND_TO_RTT_REQUEST, "id=" + id + ", accept="
+ + accept);
if (accept) {
createRttStreams();
Log.i(this, "RTT request %d accepted.", id);
@@ -3654,6 +3962,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
public void setIsVoipAudioMode(boolean audioModeIsVoip) {
+ if (isSelfManaged() && !audioModeIsVoip) {
+ Log.i(this,
+ "setIsVoipAudioMode: ignoring request to set self-managed audio to "
+ + "non-voip mode");
+ return;
+ }
if (mIsVoipAudioMode != audioModeIsVoip) {
Log.addEvent(this, LogUtils.Events.SET_VOIP_MODE, audioModeIsVoip ? "Y" : "N");
}
@@ -3678,6 +3992,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
return mCallDirection == CALL_DIRECTION_UNKNOWN;
}
+ public boolean isOutgoing() {
+ return mCallDirection == CALL_DIRECTION_OUTGOING;
+ }
+
/**
* Determines if this call is in a disconnecting state.
*
@@ -3697,19 +4015,26 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
}
/**
- * @return user handle of user initiating the outgoing call.
+ * It's possible that the target phone account isn't set when a user hasn't selected a
+ * default sim to place a call. Instead of using the user from the target phone account to
+ * associate the user with a call, we'll use mAssociatedUser instead. For MT calls, we will
+ * continue to use the target phone account user (as it's always set) and for MO calls, we will
+ * use the initiating user instead.
+ *
+ * @return user handle of user associated with the call.
*/
- public UserHandle getInitiatingUser() {
- return mInitiatingUser;
+ public UserHandle getAssociatedUser() {
+ return mAssociatedUser;
}
/**
- * Set the user handle of user initiating the outgoing call.
- * @param initiatingUser
+ * Set the user handle of user associated with the call.
+ * @param associatedUser
*/
- public void setInitiatingUser(UserHandle initiatingUser) {
- Preconditions.checkNotNull(initiatingUser);
- mInitiatingUser = initiatingUser;
+ public void setAssociatedUser(UserHandle associatedUser) {
+ Log.i(this, "Setting associated user for call");
+ Preconditions.checkNotNull(associatedUser);
+ mAssociatedUser = associatedUser;
}
static int getStateFromConnectionState(int state) {
@@ -3772,7 +4097,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
public void setRttMode(int mode) {
mRttMode = mode;
- // TODO: hook this up to CallAudioManager
+ Log.addEvent(this, LogUtils.Events.SET_RRT_MODE, "mode=" + mode);
+ // TODO: hook this up to CallAudioManager.
}
/**
@@ -3803,6 +4129,15 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
* @param extras The extras.
*/
public void onConnectionEvent(String event, Bundle extras) {
+ if (mIsTransactionalCall) {
+ // send the Event directly to the ICS via the InCallController listener
+ for (Listener l : mListeners) {
+ l.onConnectionEvent(this, event, extras);
+ }
+ // Don't run the below block since it applies to Calls that are attached to a
+ // ConnectionService
+ return;
+ }
// Don't log call quality reports; they're quite frequent and will clog the log.
if (!Connection.EVENT_CALL_QUALITY_REPORT.equals(event)) {
Log.addEvent(this, LogUtils.Events.CONNECTION_EVENT, event);
@@ -3847,6 +4182,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
l.onReceivedCallQualityReport(this, callQuality);
}
} else {
+ if (event.equals(EVENT_DISPLAY_SOS_MESSAGE) && !isEmergencyCall()) {
+ Log.w(this, "onConnectionEvent: EVENT_DISPLAY_SOS_MESSAGE is sent "
+ + "without an emergency call");
+ return;
+ }
+
for (Listener l : mListeners) {
l.onConnectionEvent(this, event, extras);
}
@@ -4162,7 +4503,20 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
mCallScreeningComponentName = callScreeningComponentName;
}
+ public void setStartFailCause(CallFailureCause cause) {
+ mCallStateChangedAtomWriter.setStartFailCause(cause);
+ }
+
+ public void increaseHeldByThisCallCount() {
+ mCallStateChangedAtomWriter.increaseHeldCallCount();
+ }
+
public void maybeOnInCallServiceTrackingChanged(boolean isTracking, boolean hasUi) {
+ if (mTransactionalService != null) {
+ Log.i(this,
+ "maybeOnInCallServiceTrackingChanged: called on TransactionalService");
+ return;
+ }
if (mConnectionService == null) {
Log.w(this, "maybeOnInCallServiceTrackingChanged() request on a call"
+ " without a connection service.");
@@ -4248,4 +4602,56 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
mDisconnectFuture = null;
}
}
+
+ /**
+ * @return {@code true} if the connection has been created by the underlying
+ * {@link ConnectionService}, {@code false} otherwise.
+ */
+ public boolean isCreateConnectionComplete() {
+ return mIsCreateConnectionComplete;
+ }
+
+ @VisibleForTesting
+ public void setIsCreateConnectionComplete(boolean isCreateConnectionComplete) {
+ mIsCreateConnectionComplete = isCreateConnectionComplete;
+ }
+
+ public boolean isStreaming() {
+ synchronized (mLock) {
+ return mIsStreaming;
+ }
+ }
+
+ public void startStreaming() {
+ if (!mIsTransactionalCall) {
+ throw new UnsupportedOperationException(
+ "Can't streaming call created by non voip apps");
+ }
+ Log.addEvent(this, LogUtils.Events.START_STREAMING);
+ synchronized (mLock) {
+ if (mIsStreaming) {
+ // ignore
+ return;
+ }
+
+ mIsStreaming = true;
+ for (Listener listener : mListeners) {
+ listener.onCallStreamingStateChanged(this, true /** isStreaming */);
+ }
+ }
+ }
+
+ public void stopStreaming() {
+ synchronized (mLock) {
+ if (!mIsStreaming) {
+ // ignore
+ return;
+ }
+ Log.addEvent(this, LogUtils.Events.STOP_STREAMING);
+ mIsStreaming = false;
+ for (Listener listener : mListeners) {
+ listener.onCallStreamingStateChanged(this, false /** isStreaming */);
+ }
+ }
+ }
}
diff --git a/src/com/android/server/telecom/CallAnomalyWatchdog.java b/src/com/android/server/telecom/CallAnomalyWatchdog.java
new file mode 100644
index 000000000..045671e25
--- /dev/null
+++ b/src/com/android/server/telecom/CallAnomalyWatchdog.java
@@ -0,0 +1,440 @@
+/*
+ * 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.LogUtils.Events.STATE_TIMEOUT;
+
+import android.provider.DeviceConfig;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.stats.CallStateChangedAtomWriter;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Watchdog class responsible for detecting potential anomalous conditions for {@link Call}s.
+ */
+public class CallAnomalyWatchdog extends CallsManagerListenerBase implements Call.Listener {
+ private final EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
+
+ /**
+ * Class used to track the call state as it pertains to the watchdog. The watchdog cares about
+ * both the call state and whether a {@link ConnectionService} has finished creating the
+ * connection.
+ */
+ public static class WatchdogCallState {
+ public final int state;
+ public final boolean isCreateConnectionComplete;
+ public final long stateStartTimeMillis;
+
+ public WatchdogCallState(int newState, boolean newIsCreateConnectionComplete,
+ long newStateStartTimeMillis) {
+ state = newState;
+ isCreateConnectionComplete = newIsCreateConnectionComplete;
+ stateStartTimeMillis = newStateStartTimeMillis;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WatchdogCallState)) return false;
+ WatchdogCallState that = (WatchdogCallState) o;
+ // don't include the state timestamp in the equality check.
+ return state == that.state
+ && isCreateConnectionComplete == that.isCreateConnectionComplete;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(state, isCreateConnectionComplete);
+ }
+
+ @Override
+ public String toString() {
+ return "[isCreateConnComplete=" + isCreateConnectionComplete + ", state="
+ + CallState.toString(state) + "]";
+ }
+
+ /**
+ * Determines if the current call is in a transitory state. A call is deemed to be in a
+ * transitory state if either {@link CallState#isTransitoryState(int)} returns true, OR
+ * if the call has been created but is not yet added to {@link CallsManager} (i.e. we are
+ * still waiting for the {@link ConnectionService} to create the connection.
+ * @return {@code true} if the call is in a transitory state, {@code false} otherwise.
+ */
+ public boolean isInTransitoryState() {
+ return CallState.isTransitoryState(state)
+ // Consider it transitory if create connection hasn't completed, EXCEPT if we
+ // are in SELECT_PHONE_ACCOUNT state since that state will depend on user input.
+ || (!isCreateConnectionComplete && state != CallState.SELECT_PHONE_ACCOUNT);
+ }
+
+ /**
+ * Determines if the current call is in an intermediate state. A call is deemed to be in
+ * an intermediate state if either {@link CallState#isIntermediateState(int)} returns true,
+ * AND the call has been created to the connection.
+ * @return {@code true} if the call is in a intermediate state, {@code false} otherwise.
+ */
+ public boolean isInIntermediateState() {
+ return CallState.isIntermediateState(state) && isCreateConnectionComplete;
+ }
+ }
+
+ // Handler for tracking pending timeouts.
+ private final ScheduledExecutorService mScheduledExecutorService;
+ private final TelecomSystem.SyncRoot mLock;
+ private final Timeouts.Adapter mTimeoutAdapter;
+ private final ClockProxy mClockProxy;
+ 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);
+ private final Map<Call, WatchdogCallState> mWatchdogCallStateMap = new ConcurrentHashMap<>(2);
+ // Track the calls which are pending destruction.
+ // TODO: enhance to handle the case where a call never gets destroyed.
+ private final Set<Call> mCallsPendingDestruction = Collections.newSetFromMap(
+ new ConcurrentHashMap<>(2));
+ private final LocalLog mLocalLog = new LocalLog(20);
+
+ /**
+ * Enables the action to disconnect the call when the Transitory state and Intermediate state
+ * time expires.
+ */
+ private static final String ENABLE_DISCONNECT_CALL_ON_STUCK_STATE =
+ "enable_disconnect_call_on_stuck_state";
+ /**
+ * Anomaly Report UUIDs and corresponding event descriptions specific to CallAnomalyWatchdog.
+ */
+ public static final UUID WATCHDOG_DISCONNECTED_STUCK_CALL_UUID =
+ UUID.fromString("4b093985-c78f-45e3-a9fe-5319f397b025");
+ public static final String WATCHDOG_DISCONNECTED_STUCK_CALL_MSG =
+ "Telecom CallAnomalyWatchdog caught and disconnected a stuck/zombie call.";
+ public static final UUID WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_UUID =
+ 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.";
+
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+ mAnomalyReporter = mAnomalyReporterAdapter;
+ }
+
+ public CallAnomalyWatchdog(ScheduledExecutorService executorService,
+ TelecomSystem.SyncRoot lock,
+ Timeouts.Adapter timeoutAdapter, ClockProxy clockProxy,
+ EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
+ mScheduledExecutorService = executorService;
+ mLock = lock;
+ mTimeoutAdapter = timeoutAdapter;
+ mClockProxy = clockProxy;
+ mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
+ }
+
+ /**
+ * Start tracking a call that we're waiting for a ConnectionService to create.
+ * @param call the call.
+ */
+ @Override
+ public void onStartCreateConnection(Call call) {
+ maybeTrackCall(call);
+ call.addListener(this);
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ maybeTrackCall(call);
+ }
+
+ /**
+ * Override of {@link CallsManagerListenerBase} to track when calls have failed to be created by
+ * a ConnectionService. These calls should no longer be tracked by the CallAnomalyWatchdog.
+ * @param call the call
+ */
+ @Override
+ public void onCreateConnectionFailed(Call call) {
+ Log.i(this, "onCreateConnectionFailed: call=%s", call.toString());
+ stopTrackingCall(call);
+ }
+
+ /**
+ * Override of {@link CallsManagerListenerBase} to track when calls are removed
+ * @param call the call
+ */
+ @Override
+ public void onCallRemoved(Call call) {
+ Log.i(this, "onCallRemoved: call=%s", call.toString());
+ stopTrackingCall(call);
+ }
+
+ /**
+ * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track
+ * call state changes.
+ * @param call the call
+ * @param oldState its old state
+ * @param newState the new state
+ */
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ Log.i(this, "onCallStateChanged: call=%s", call.toString());
+ maybeTrackCall(call);
+ }
+
+ /**
+ * Override of {@link Call.Listener} so we can capture successful creation of calls.
+ * @param call the call
+ * @param callState the state the call is now in
+ */
+ @Override
+ public void onSuccessfulOutgoingCall(Call call, int callState) {
+ maybeTrackCall(call);
+ }
+
+ /**
+ * Override of {@link Call.Listener} so we can capture failed call creation.
+ * @param call the call
+ * @param disconnectCause the disconnect cause
+ */
+ @Override
+ public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {
+ Log.i(this, "onFailedOutgoingCall: call=%s", call.toString());
+ stopTrackingCall(call);
+ }
+
+ /**
+ * Override of {@link Call.Listener} so we can capture successful creation of calls
+ * @param call the call
+ */
+ @Override
+ public void onSuccessfulIncomingCall(Call call) {
+ maybeTrackCall(call);
+ }
+
+ /**
+ * Override of {@link Call.Listener} so we can capture failed call creation.
+ * @param call the call
+ */
+ @Override
+ public void onFailedIncomingCall(Call call) {
+ Log.i(this, "onFailedIncomingCall: call=%s", call.toString());
+ stopTrackingCall(call);
+ }
+
+ /**
+ * Helper method used to stop CallAnomalyWatchdog from tracking or destroying the call.
+ * @param call the call.
+ */
+ private void stopTrackingCall(Call call) {
+ if (mScheduledFutureMap.containsKey(call)) {
+ ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+ existingTimeout.cancel(false /* cancelIfRunning */);
+ mScheduledFutureMap.remove(call);
+ }
+ if (mCallsPendingDestruction.contains(call)) {
+ mCallsPendingDestruction.remove(call);
+ }
+ if (mWatchdogCallStateMap.containsKey(call)) {
+ mWatchdogCallStateMap.remove(call);
+ }
+ call.removeListener(this);
+ }
+
+ /**
+ * Given a {@link Call}, potentially post a cleanup task to track when the call has been in a
+ * transitory state too long.
+ * @param call the call.
+ */
+ private void maybeTrackCall(Call call) {
+ final WatchdogCallState currentState = mWatchdogCallStateMap.get(call);
+ final WatchdogCallState newState = new WatchdogCallState(call.getState(),
+ call.isCreateConnectionComplete(), mClockProxy.elapsedRealtime());
+ if (Objects.equals(currentState, newState)) {
+ // No state change; skip.
+ return;
+ }
+ mWatchdogCallStateMap.put(call, newState);
+
+ // The call's state has changed, so we will remove any existing state cleanup tasks.
+ if (mScheduledFutureMap.containsKey(call)) {
+ ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+ existingTimeout.cancel(false /* cancelIfRunning */);
+ mScheduledFutureMap.remove(call);
+ }
+
+ Log.i(this, "maybePostCleanupTask; callId=%s, state=%s, createConnComplete=%b",
+ call.getId(), CallState.toString(call.getState()),
+ call.isCreateConnectionComplete());
+
+ long timeoutMillis = getTimeoutMillis(call, newState);
+ boolean isEnabledDisconnect = isEnabledDisconnectForStuckCall();
+ // If the call is now in a transitory or intermediate state, post a new cleanup task.
+ if (timeoutMillis > 0) {
+ Runnable cleanupRunnable = getCleanupRunnable(call, newState, timeoutMillis,
+ isEnabledDisconnect);
+
+ // Post cleanup to the executor service and cache the future, so we can cancel it if
+ // needed.
+ ScheduledFuture<?> future = mScheduledExecutorService.schedule(cleanupRunnable,
+ timeoutMillis, TimeUnit.MILLISECONDS);
+ mScheduledFutureMap.put(call, future);
+ }
+ }
+
+ public long getTimeoutMillis(Call call, WatchdogCallState state) {
+ boolean isVoip = call.getIsVoipAudioMode();
+ boolean isEmergency = call.isEmergencyCall();
+
+ if (state.isInTransitoryState()) {
+ if (isVoip) {
+ return (isEmergency) ?
+ mTimeoutAdapter.getVoipEmergencyCallTransitoryStateTimeoutMillis() :
+ mTimeoutAdapter.getVoipCallTransitoryStateTimeoutMillis();
+ }
+
+ return (isEmergency) ?
+ mTimeoutAdapter.getNonVoipEmergencyCallTransitoryStateTimeoutMillis() :
+ mTimeoutAdapter.getNonVoipCallTransitoryStateTimeoutMillis();
+ }
+
+ if (state.isInIntermediateState()) {
+ if (isVoip) {
+ return (isEmergency) ?
+ mTimeoutAdapter.getVoipEmergencyCallIntermediateStateTimeoutMillis() :
+ mTimeoutAdapter.getVoipCallIntermediateStateTimeoutMillis();
+ }
+
+ return (isEmergency) ?
+ mTimeoutAdapter.getNonVoipEmergencyCallIntermediateStateTimeoutMillis() :
+ mTimeoutAdapter.getNonVoipCallIntermediateStateTimeoutMillis();
+ }
+
+ return 0;
+ }
+
+ private Runnable getCleanupRunnable(Call call, WatchdogCallState newState, long timeoutMillis,
+ boolean isEnabledDisconnect) {
+ Runnable cleanupRunnable = new android.telecom.Logging.Runnable("CAW.mR", mLock) {
+ @Override
+ public void loggedRun() {
+ // If we're already pending a cleanup due to a state violation for this call.
+ if (mCallsPendingDestruction.contains(call)) {
+ return;
+ }
+ // Ensure that at timeout we are still in the original state when we posted the
+ // timeout.
+ final WatchdogCallState expiredState = new WatchdogCallState(call.getState(),
+ call.isCreateConnectionComplete(), mClockProxy.elapsedRealtime());
+ if (expiredState.equals(newState)
+ && getDurationInCurrentStateMillis(newState) > timeoutMillis) {
+ // The call has been in this transitory or intermediate state too long,
+ // so disconnect it and destroy it.
+ Log.addEvent(call, STATE_TIMEOUT, newState);
+ mLocalLog.log("STATE_TIMEOUT; callId=" + call.getId() + " in state "
+ + newState);
+ if (call.isEmergencyCall()){
+ mAnomalyReporter.reportAnomaly(
+ WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_UUID,
+ WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_MSG);
+ mEmergencyCallDiagnosticLogger.reportStuckCall(call);
+ } else {
+ mAnomalyReporter.reportAnomaly(
+ WATCHDOG_DISCONNECTED_STUCK_CALL_UUID,
+ WATCHDOG_DISCONNECTED_STUCK_CALL_MSG);
+ }
+
+ if (isEnabledDisconnect) {
+ call.setOverrideDisconnectCauseCode(
+ new DisconnectCause(DisconnectCause.ERROR, "state_timeout"));
+ call.disconnect("State timeout");
+ } else {
+ writeCallStateChangedAtom(call);
+ }
+
+ mCallsPendingDestruction.add(call);
+ if (mWatchdogCallStateMap.containsKey(call)) {
+ mWatchdogCallStateMap.remove(call);
+ }
+ }
+ mScheduledFutureMap.remove(call);
+ }
+ }.prepare();
+ return cleanupRunnable;
+ }
+
+ /**
+ * Returns whether the action to disconnect the call when the Transitory state and
+ * Intermediate state time expires is enabled or disabled.
+ * @return {@code true} if the action is enabled, {@code false} if the action is disabled.
+ */
+ private boolean isEnabledDisconnectForStuckCall() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TELEPHONY,
+ ENABLE_DISCONNECT_CALL_ON_STUCK_STATE, false);
+ }
+
+ /**
+ * Determines how long a call has been in a specific state.
+ * @param state the call state.
+ * @return the time in the state, in millis.
+ */
+ private long getDurationInCurrentStateMillis(WatchdogCallState state) {
+ return mClockProxy.elapsedRealtime() - state.stateStartTimeMillis;
+ }
+
+ private void writeCallStateChangedAtom(Call call) {
+ new CallStateChangedAtomWriter()
+ .setDisconnectCause(call.getDisconnectCause())
+ .setSelfManaged(call.isSelfManaged())
+ .setExternalCall(call.isExternalCall())
+ .setEmergencyCall(call.isEmergencyCall())
+ .write(call.getState());
+ }
+
+ /**
+ * Dumps the state of the {@link CallAnomalyWatchdog}.
+ *
+ * @param pw The {@code IndentingPrintWriter} to write the state to.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("Anomaly log:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ pw.print("Pending timeouts: ");
+ pw.println(mScheduledFutureMap.keySet().stream().map(c -> c.getId()).collect(
+ Collectors.joining(",")));
+ pw.print("Pending destruction: ");
+ pw.println(mCallsPendingDestruction.stream().map(c -> c.getId()).collect(
+ Collectors.joining(",")));
+ }
+
+ @VisibleForTesting
+ public int getNumberOfScheduledTimeouts() {
+ return mScheduledFutureMap.size();
+ }
+}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 1863cdebc..ff76b9e89 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -17,10 +17,13 @@
package com.android.server.telecom;
import android.annotation.NonNull;
+import android.content.Context;
import android.media.IAudioService;
import android.media.ToneGenerator;
+import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
+import android.telecom.PhoneAccount;
import android.telecom.VideoProfile;
import android.util.SparseArray;
@@ -58,6 +61,7 @@ public class CallAudioManager extends CallsManagerListenerBase {
private final RingbackPlayer mRingbackPlayer;
private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
+ private Call mStreamingCall;
private Call mForegroundCall;
private boolean mIsTonePlaying = false;
private boolean mIsDisconnectedTonePlaying = false;
@@ -75,6 +79,7 @@ public class CallAudioManager extends CallsManagerListenerBase {
mRingingCalls = new LinkedHashSet<>(1);
mHoldingCalls = new LinkedHashSet<>(1);
mAudioProcessingCalls = new LinkedHashSet<>(1);
+ mStreamingCall = null;
mCalls = new HashSet<>();
mCallStateToCalls = new SparseArray<LinkedHashSet<Call>>() {{
put(CallState.CONNECTING, mActiveDialingOrConnectingCalls);
@@ -141,6 +146,9 @@ public class CallAudioManager extends CallsManagerListenerBase {
@Override
public void onCallRemoved(Call call) {
+ if (mStreamingCall == call) {
+ mStreamingCall = null;
+ }
if (shouldIgnoreCallForAudio(call)) {
return; // Don't do audio handling for calls in a conference, or external calls.
}
@@ -219,6 +227,36 @@ public class CallAudioManager extends CallsManagerListenerBase {
}
/**
+ * Handles the changes to the streaming state of a call.
+ * @param call The call
+ * @param isStreaming {@code true} if the call is streaming, {@code false} otherwise
+ */
+ @Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ if (isStreaming) {
+ if (mStreamingCall == null) {
+ mStreamingCall = call;
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.START_CALL_STREAMING,
+ makeArgsForModeStateMachine());
+ } else {
+ Log.w(LOG_TAG, "Unexpected streaming call request for call %s while call "
+ + "%s is streaming.", call.getId(), mStreamingCall.getId());
+ }
+ } else {
+ if (mStreamingCall == call) {
+ mStreamingCall = null;
+ mCallAudioModeStateMachine.sendMessageWithArgs(
+ CallAudioModeStateMachine.STOP_CALL_STREAMING,
+ makeArgsForModeStateMachine());
+ } else {
+ Log.w(LOG_TAG, "Unexpected call streaming stop request for call %s while this call "
+ + "is not streaming.", call.getId());
+ }
+ }
+ }
+
+ /**
* Determines if {@link CallAudioManager} should do any audio routing operations for a call.
* We ignore child calls of a conference and external calls for audio routing purposes.
*
@@ -410,7 +448,8 @@ public class CallAudioManager extends CallsManagerListenerBase {
* @param bluetoothAddress the address of the desired bluetooth device, if route is
* {@link CallAudioState#ROUTE_BLUETOOTH}.
*/
- void setAudioRoute(int route, String bluetoothAddress) {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public void setAudioRoute(int route, String bluetoothAddress) {
Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
switch (route) {
case CallAudioState.ROUTE_BLUETOOTH:
@@ -450,15 +489,34 @@ public class CallAudioManager extends CallsManagerListenerBase {
CallAudioRouteStateMachine.INCLUDE_BLUETOOTH_IN_BASELINE);
}
- void silenceRingers() {
+ Set<UserHandle> silenceRingers(Context context, UserHandle callingUser,
+ boolean hasCrossUserPermission) {
+ // Store all users from calls that were silenced so that we can silence the
+ // InCallServices which are associated with those users.
+ Set<UserHandle> userHandles = new HashSet<>();
+ boolean allCallSilenced = true;
synchronized (mCallsManager.getLock()) {
for (Call call : mRingingCalls) {
+ UserHandle userFromCall = call.getAssociatedUser();
+ // Do not try to silence calls when calling user is different from the phone account
+ // user, the account does not have CAPABILITY_MULTI_USER enabled, or if the user
+ // does not have the INTERACT_ACROSS_USERS permission enabled.
+ if (!hasCrossUserPermission && !mCallsManager
+ .isCallVisibleForUser(call, callingUser)) {
+ allCallSilenced = false;
+ continue;
+ }
+ userHandles.add(userFromCall);
call.silence();
}
- mRinger.stopRinging();
- mRinger.stopCallWaiting();
+ // If all the calls were silenced, we can stop the ringer.
+ if (allCallSilenced) {
+ mRinger.stopRinging();
+ mRinger.stopCallWaiting();
+ }
}
+ return userHandles;
}
public boolean isRingtonePlaying() {
@@ -737,6 +795,7 @@ public class CallAudioManager extends CallsManagerListenerBase {
.setHasHoldingCalls(mHoldingCalls.size() > 0)
.setHasAudioProcessingCalls(mAudioProcessingCalls.size() > 0)
.setIsTonePlaying(mIsTonePlaying)
+ .setIsStreaming((mStreamingCall != null) && (!mStreamingCall.isDisconnected()))
.setForegroundCallIsVoip(
mForegroundCall != null && isCallVoip(mForegroundCall))
.setSession(Log.createSubsession()).build();
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index a1c5f4bdd..9ad9094ef 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -19,12 +19,12 @@ package com.android.server.telecom;
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;
import android.util.LocalLog;
import android.util.SparseArray;
-
import com.android.internal.util.IState;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.State;
@@ -50,17 +50,19 @@ public class CallAudioModeStateMachine extends StateMachine {
public boolean hasAudioProcessingCalls;
public boolean isTonePlaying;
public boolean foregroundCallIsVoip;
+ public boolean isStreaming;
public Session session;
private MessageArgs(boolean hasActiveOrDialingCalls, boolean hasRingingCalls,
boolean hasHoldingCalls, boolean hasAudioProcessingCalls, boolean isTonePlaying,
- boolean foregroundCallIsVoip, Session session) {
+ boolean foregroundCallIsVoip, boolean isStreaming, Session session) {
this.hasActiveOrDialingCalls = hasActiveOrDialingCalls;
this.hasRingingCalls = hasRingingCalls;
this.hasHoldingCalls = hasHoldingCalls;
this.hasAudioProcessingCalls = hasAudioProcessingCalls;
this.isTonePlaying = isTonePlaying;
this.foregroundCallIsVoip = foregroundCallIsVoip;
+ this.isStreaming = isStreaming;
this.session = session;
}
@@ -73,6 +75,7 @@ public class CallAudioModeStateMachine extends StateMachine {
", hasAudioProcessingCalls=" + hasAudioProcessingCalls +
", isTonePlaying=" + isTonePlaying +
", foregroundCallIsVoip=" + foregroundCallIsVoip +
+ ", isStreaming=" + isStreaming +
", session=" + session +
'}';
}
@@ -84,6 +87,7 @@ public class CallAudioModeStateMachine extends StateMachine {
private boolean mHasAudioProcessingCalls;
private boolean mIsTonePlaying;
private boolean mForegroundCallIsVoip;
+ private boolean mIsStreaming;
private Session mSession;
public Builder setHasActiveOrDialingCalls(boolean hasActiveOrDialingCalls) {
@@ -121,9 +125,15 @@ public class CallAudioModeStateMachine extends StateMachine {
return this;
}
+ public Builder setIsStreaming(boolean isStraeming) {
+ mIsStreaming = isStraeming;
+ return this;
+ }
+
public MessageArgs build() {
return new MessageArgs(mHasActiveOrDialingCalls, mHasRingingCalls, mHasHoldingCalls,
- mHasAudioProcessingCalls, mIsTonePlaying, mForegroundCallIsVoip, mSession);
+ mHasAudioProcessingCalls, mIsTonePlaying, mForegroundCallIsVoip,
+ mIsStreaming, mSession);
}
}
}
@@ -138,7 +148,8 @@ public class CallAudioModeStateMachine extends StateMachine {
public static final int ENTER_RING_FOCUS_FOR_TESTING = 4;
public static final int ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING = 5;
public static final int ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING = 6;
- public static final int ABANDON_FOCUS_FOR_TESTING = 7;
+ public static final int ENTER_STREAMING_FOCUS_FOR_TESTING = 7;
+ public static final int ABANDON_FOCUS_FOR_TESTING = 8;
public static final int NO_MORE_ACTIVE_OR_DIALING_CALLS = 1001;
public static final int NO_MORE_RINGING_CALLS = 1002;
@@ -161,6 +172,9 @@ public class CallAudioModeStateMachine extends StateMachine {
// to release focus for other apps to take over.
public static final int AUDIO_OPERATIONS_COMPLETE = 6001;
+ public static final int START_CALL_STREAMING = 7001;
+ public static final int STOP_CALL_STREAMING = 7002;
+
public static final int RUN_RUNNABLE = 9001;
private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
@@ -183,6 +197,8 @@ public class CallAudioModeStateMachine extends StateMachine {
put(FOREGROUND_VOIP_MODE_CHANGE, "FOREGROUND_VOIP_MODE_CHANGE");
put(RINGER_MODE_CHANGE, "RINGER_MODE_CHANGE");
put(AUDIO_OPERATIONS_COMPLETE, "AUDIO_OPERATIONS_COMPLETE");
+ put(START_CALL_STREAMING, "START_CALL_STREAMING");
+ put(STOP_CALL_STREAMING, "STOP_CALL_STREAMING");
put(RUN_RUNNABLE, "RUN_RUNNABLE");
}};
@@ -193,6 +209,7 @@ public class CallAudioModeStateMachine extends StateMachine {
AudioProcessingFocusState.class.getSimpleName();
public static final String CALL_STATE_NAME = SimCallFocusState.class.getSimpleName();
public static final String RING_STATE_NAME = RingingFocusState.class.getSimpleName();
+ public static final String STREAMING_STATE_NAME = StreamingFocusState.class.getSimpleName();
public static final String COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName();
private class BaseState extends State {
@@ -214,6 +231,9 @@ public class CallAudioModeStateMachine extends StateMachine {
case ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING:
transitionTo(mAudioProcessingFocusState);
return HANDLED;
+ case ENTER_STREAMING_FOCUS_FOR_TESTING:
+ transitionTo(mStreamingFocusState);
+ return HANDLED;
case ABANDON_FOCUS_FOR_TESTING:
transitionTo(mUnfocusedState);
return HANDLED;
@@ -280,6 +300,9 @@ public class CallAudioModeStateMachine extends StateMachine {
" Args are: \n" + args.toString());
transitionTo(mOtherFocusState);
return HANDLED;
+ case START_CALL_STREAMING:
+ transitionTo(mStreamingFocusState);
+ return HANDLED;
case TONE_STARTED_PLAYING:
// This shouldn't happen either, but perform the action anyway.
Log.w(LOG_TAG, "Tone started playing unexpectedly. Args are: \n"
@@ -353,6 +376,9 @@ public class CallAudioModeStateMachine extends StateMachine {
Log.w(LOG_TAG, "Tone started playing unexpectedly. Args are: \n"
+ args.toString());
return HANDLED;
+ case START_CALL_STREAMING:
+ transitionTo(mStreamingFocusState);
+ return HANDLED;
case AUDIO_OPERATIONS_COMPLETE:
Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING");
mAudioManager.abandonAudioFocusForCall();
@@ -370,26 +396,33 @@ public class CallAudioModeStateMachine extends StateMachine {
private boolean mHasFocus = false;
private void tryStartRinging() {
- 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(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");
+ 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;
}
- mCallAudioManager.setCallAudioRouteFocusState(
+
+ if (mCallAudioManager.startRinging()) {
+ 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");
+ mHasFocus = true;
+ } else {
+ Log.i(
+ LOG_TAG, "RINGING state, try start ringing but not acquiring audio focus");
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_AUDIO);
}
}
@@ -618,6 +651,82 @@ public class CallAudioModeStateMachine extends StateMachine {
Log.w(LOG_TAG, "Should not be seeing AUDIO_OPERATIONS_COMPLETE in a focused"
+ " state");
return HANDLED;
+ case START_CALL_STREAMING:
+ transitionTo(mStreamingFocusState);
+ return HANDLED;
+ default:
+ // The forced focus switch commands are handled by BaseState.
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private class StreamingFocusState extends BaseState {
+ @Override
+ public void enter() {
+ Log.i(LOG_TAG, "Audio focus entering streaming state");
+ mLocalLog.log("Enter Streaming");
+ mLocalLog.log("Mode MODE_COMMUNICATION_REDIRECT");
+ mAudioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT);
+ mMostRecentMode = AudioManager.MODE_NORMAL;
+ mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
+ mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.STREAMING_FORCE_ENABLED);
+ }
+
+ private void preExit() {
+ mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ MessageArgs args = (MessageArgs) msg.obj;
+ switch (msg.what) {
+ case NO_MORE_ACTIVE_OR_DIALING_CALLS:
+ // Switch to either ringing, holding, or inactive
+ transitionTo(calculateProperStateFromArgs(args));
+ return HANDLED;
+ case NO_MORE_RINGING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NO_MORE_HOLDING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NO_MORE_AUDIO_PROCESSING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NEW_ACTIVE_OR_DIALING_CALL:
+ // Only possible for emergency call
+ BaseState destState = calculateProperStateFromArgs(args);
+ if (destState != this) {
+ preExit();
+ transitionTo(destState);
+ }
+ return HANDLED;
+ case NEW_RINGING_CALL:
+ // Only possible for emergency call
+ preExit();
+ transitionTo(mRingingFocusState);
+ return HANDLED;
+ case NEW_HOLDING_CALL:
+ // Do nothing.
+ return HANDLED;
+ case NEW_AUDIO_PROCESSING_CALL:
+ // Do nothing.
+ return HANDLED;
+ case START_CALL_STREAMING:
+ // Can happen as a duplicate message
+ return HANDLED;
+ case TONE_STARTED_PLAYING:
+ // Do nothing.
+ return HANDLED;
+ case STOP_CALL_STREAMING:
+ transitionTo(calculateProperStateFromArgs(args));
+ return HANDLED;
default:
// The forced focus switch commands are handled by BaseState.
return NOT_HANDLED;
@@ -700,6 +809,7 @@ public class CallAudioModeStateMachine extends StateMachine {
private final BaseState mSimCallFocusState = new SimCallFocusState();
private final BaseState mVoipCallFocusState = new VoipCallFocusState();
private final BaseState mAudioProcessingFocusState = new AudioProcessingFocusState();
+ private final BaseState mStreamingFocusState = new StreamingFocusState();
private final BaseState mOtherFocusState = new OtherFocusState();
private final AudioManager mAudioManager;
@@ -738,6 +848,7 @@ public class CallAudioModeStateMachine extends StateMachine {
addState(mSimCallFocusState);
addState(mVoipCallFocusState);
addState(mAudioProcessingFocusState);
+ addState(mStreamingFocusState);
addState(mOtherFocusState);
setInitialState(mUnfocusedState);
start();
@@ -747,6 +858,7 @@ public class CallAudioModeStateMachine extends StateMachine {
.setHasHoldingCalls(false)
.setIsTonePlaying(false)
.setForegroundCallIsVoip(false)
+ .setIsStreaming(false)
.setSession(Log.createSubsession())
.build());
}
@@ -800,12 +912,15 @@ public class CallAudioModeStateMachine extends StateMachine {
// switch to the appropriate focus.
// Otherwise abandon focus.
- // The order matters here. If there are active calls, holding focus for them takes priority.
- // After that, we want to prioritize holding calls over ringing calls so that when a
- // call-waiting call gets answered, there's no transition in and out of the ringing focus
- // state. After that, we want tones since we actually hold focus during them, then the
- // audio processing state because that will release focus.
- if (args.hasActiveOrDialingCalls) {
+ // The order matters here. If there is streaming call, holding streaming route for them
+ // takes priority. After that, holding focus for active calls takes priority. After that, we
+ // want to prioritize holding calls over ringing calls so that when a call-waiting call gets
+ // answered, there's no transition in and out of the ringing focus state. After that, we
+ // want tones since we actually hold focus during them, then the audio processing state
+ // because that will release focus.
+ if (args.isStreaming) {
+ return mSimCallFocusState;
+ } else if (args.hasActiveOrDialingCalls) {
if (args.foregroundCallIsVoip) {
return mVoipCallFocusState;
} else {
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 2ed7d9553..4a03726c6 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -48,6 +48,8 @@ import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import java.util.Collection;
import java.util.HashMap;
import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
/**
* This class describes the available routes of a call as a state machine.
@@ -80,14 +82,16 @@ public class CallAudioRouteStateMachine extends StateMachine {
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl) {
+ int earpieceControl,
+ Executor asyncTaskExecutor) {
return new CallAudioRouteStateMachine(context,
callsManager,
bluetoothManager,
wiredHeadsetManager,
statusBarNotifier,
audioServiceFactory,
- earpieceControl);
+ earpieceControl,
+ asyncTaskExecutor);
}
}
/** Values for CallAudioRouteStateMachine constructor's earPieceRouting arg. */
@@ -107,6 +111,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
/** Direct the audio stream through the device's speakerphone. */
public static final int ROUTE_SPEAKER = CallAudioState.ROUTE_SPEAKER;
+ /** 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;
@@ -128,6 +135,10 @@ public class CallAudioRouteStateMachine extends StateMachine {
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;
@@ -544,6 +555,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
case DISCONNECT_DOCK:
// Nothing to do here
return HANDLED;
+ case STREAMING_FORCE_ENABLED:
+ transitionTo(mStreamingState);
+ return HANDLED;
default:
return NOT_HANDLED;
}
@@ -628,6 +642,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
mCallAudioManager.notifyAudioOperationsComplete();
}
return HANDLED;
+ case STREAMING_FORCE_ENABLED:
+ transitionTo(mStreamingState);
+ return HANDLED;
default:
return NOT_HANDLED;
}
@@ -1105,6 +1122,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
case DISCONNECT_DOCK:
// Nothing to do here
return HANDLED;
+ case STREAMING_FORCE_ENABLED:
+ transitionTo(mStreamingState);
+ return HANDLED;
default:
return NOT_HANDLED;
}
@@ -1330,12 +1350,74 @@ public class CallAudioRouteStateMachine extends StateMachine {
case DISCONNECT_DOCK:
sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
return HANDLED;
+ case STREAMING_FORCE_ENABLED:
+ transitionTo(mStreamingState);
+ return HANDLED;
default:
return NOT_HANDLED;
}
}
}
+ class StreamingState extends AudioState {
+ @Override
+ public void enter() {
+ super.enter();
+ updateSystemAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ setSystemAudioState(mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public int getRouteCode() {
+ return CallAudioState.ROUTE_STREAMING;
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ case USER_SWITCH_EARPIECE:
+ case SPEAKER_OFF:
+ // Nothing to do here
+ return HANDLED;
+ case SPEAKER_ON:
+ // fall through
+ case BT_AUDIO_CONNECTED:
+ case SWITCH_BLUETOOTH:
+ case USER_SWITCH_BLUETOOTH:
+ case SWITCH_HEADSET:
+ case USER_SWITCH_HEADSET:
+ case SWITCH_SPEAKER:
+ case USER_SWITCH_SPEAKER:
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ reinitialize();
+ mCallAudioManager.notifyAudioOperationsComplete();
+ }
+ return HANDLED;
+ case STREAMING_FORCE_DISABLED:
+ reinitialize();
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
private final BroadcastReceiver mMuteChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -1398,6 +1480,9 @@ public class CallAudioRouteStateMachine extends StateMachine {
private final QuiescentHeadsetRoute mQuiescentHeadsetRoute = new QuiescentHeadsetRoute();
private final QuiescentBluetoothRoute mQuiescentBluetoothRoute = new QuiescentBluetoothRoute();
private final QuiescentSpeakerRoute mQuiescentSpeakerRoute = new QuiescentSpeakerRoute();
+ private final StreamingState mStreamingState = new StreamingState();
+
+ private final Executor mAsyncTaskExecutor;
/**
* A few pieces of hidden state. Used to avoid exponential explosion of number of explicit
@@ -1437,7 +1522,8 @@ public class CallAudioRouteStateMachine extends StateMachine {
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl) {
+ int earpieceControl,
+ Executor asyncTaskExecutor) {
super(NAME);
mContext = context;
mCallsManager = callsManager;
@@ -1447,7 +1533,7 @@ public class CallAudioRouteStateMachine extends StateMachine {
mStatusBarNotifier = statusBarNotifier;
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
-
+ mAsyncTaskExecutor = asyncTaskExecutor;
createStates(earpieceControl);
}
@@ -1459,7 +1545,7 @@ public class CallAudioRouteStateMachine extends StateMachine {
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl, Looper looper) {
+ int earpieceControl, Looper looper, Executor asyncTaskExecutor) {
super(NAME, looper);
mContext = context;
mCallsManager = callsManager;
@@ -1469,6 +1555,7 @@ public class CallAudioRouteStateMachine extends StateMachine {
mStatusBarNotifier = statusBarNotifier;
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
+ mAsyncTaskExecutor = asyncTaskExecutor;
createStates(earpieceControl);
}
@@ -1494,6 +1581,7 @@ public class CallAudioRouteStateMachine extends StateMachine {
addState(mQuiescentHeadsetRoute);
addState(mQuiescentBluetoothRoute);
addState(mQuiescentSpeakerRoute);
+ addState(mStreamingState);
mStateNameToRouteCode = new HashMap<>(8);
@@ -1506,12 +1594,14 @@ public class CallAudioRouteStateMachine extends StateMachine {
mStateNameToRouteCode.put(mActiveBluetoothRoute.getName(), ROUTE_BLUETOOTH);
mStateNameToRouteCode.put(mActiveHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
mStateNameToRouteCode.put(mActiveSpeakerRoute.getName(), ROUTE_SPEAKER);
+ mStateNameToRouteCode.put(mStreamingState.getName(), ROUTE_STREAMING);
mRouteCodeToQuiescentState = new HashMap<>(4);
mRouteCodeToQuiescentState.put(ROUTE_EARPIECE, mQuiescentEarpieceRoute);
mRouteCodeToQuiescentState.put(ROUTE_BLUETOOTH, mQuiescentBluetoothRoute);
mRouteCodeToQuiescentState.put(ROUTE_SPEAKER, mQuiescentSpeakerRoute);
mRouteCodeToQuiescentState.put(ROUTE_WIRED_HEADSET, mQuiescentHeadsetRoute);
+ mRouteCodeToQuiescentState.put(ROUTE_STREAMING, mStreamingState);
}
public void setCallAudioManager(CallAudioManager callAudioManager) {
@@ -1539,15 +1629,23 @@ public class CallAudioRouteStateMachine extends StateMachine {
mAvailableRoutes = mDeviceSupportedRoutes & getCurrentCallSupportedRoutes();
mIsMuted = initState.isMuted();
mWasOnSpeaker = false;
- mContext.registerReceiver(mMuteChangeReceiver,
- new IntentFilter(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED));
- mContext.registerReceiver(mMuteChangeReceiver,
- new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION));
- mContext.registerReceiver(mSpeakerPhoneChangeReceiver,
- new IntentFilter(AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED));
+ IntentFilter micMuteChangedFilter = new IntentFilter(
+ AudioManager.ACTION_MICROPHONE_MUTE_CHANGED);
+ micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ mContext.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter);
+
+ IntentFilter muteChangedFilter = new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+ muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ mContext.registerReceiver(mMuteChangeReceiver, muteChangedFilter);
+
+ IntentFilter speakerChangedFilter = new IntentFilter(
+ AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
+ speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ mContext.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
mStatusBarNotifier.notifyMute(initState.isMuted());
- mStatusBarNotifier.notifySpeakerphone(initState.getRoute() == CallAudioState.ROUTE_SPEAKER);
+ // We used to call mStatusBarNotifier.notifySpeakerphone, but that makes no sense as there
+ // is never a call at this boot (init) time.
setInitialState(mRouteCodeToQuiescentState.get(initState.getRoute()));
start();
}
@@ -1640,6 +1738,10 @@ public class CallAudioRouteStateMachine extends StateMachine {
private void setSpeakerphoneOn(boolean on) {
Log.i(this, "turning speaker phone %s", on);
+ 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) {
@@ -1655,11 +1757,15 @@ public class CallAudioRouteStateMachine extends StateMachine {
}
} else {
AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
- if (curDevice != null && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ if (curDevice != null
+ && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
mAudioManager.clearCommunicationDevice();
}
}
- mStatusBarNotifier.notifySpeakerphone(speakerOn);
+ final boolean isSpeakerOn = speakerOn;
+ mAsyncTaskExecutor.execute(() -> {
+ mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && isSpeakerOn);
+ });
}
private void setBluetoothOn(String address) {
@@ -1764,16 +1870,18 @@ public class CallAudioRouteStateMachine extends StateMachine {
if (force || !newCallAudioState.equals(mLastKnownCallAudioState)) {
mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
mCallsManager.onCallAudioStateChanged(mLastKnownCallAudioState, newCallAudioState);
- updateAudioForForegroundCall(newCallAudioState);
+ updateAudioStateForTrackedCalls(newCallAudioState);
mLastKnownCallAudioState = newCallAudioState;
}
}
}
- private void updateAudioForForegroundCall(CallAudioState newCallAudioState) {
- Call call = mCallsManager.getForegroundCall();
- if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) {
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ }
}
}
diff --git a/src/com/android/server/telecom/CallDiagnosticServiceController.java b/src/com/android/server/telecom/CallDiagnosticServiceController.java
index 7bd7288e7..6c7ee38af 100644
--- a/src/com/android/server/telecom/CallDiagnosticServiceController.java
+++ b/src/com/android/server/telecom/CallDiagnosticServiceController.java
@@ -25,7 +25,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.IBinder;
@@ -86,17 +85,19 @@ public class CallDiagnosticServiceController extends CallsManagerListenerBase {
* Listens for changes to extras reported by a Telecom {@link Call}.
*
* Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
- * so we will only trigger an update of the call information if the source of the extras
- * change was a {@link ConnectionService}.
+ * so we will only trigger an update of the call information if the source of the
+ * extras change was a {@link ConnectionService}.
*
- * @param call The call.
- * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
+ * @param call The call.
+ * @param source The source of the extras change
+ * ({@link Call#SOURCE_CONNECTION_SERVICE} or
* {@link Call#SOURCE_INCALL_SERVICE}).
* @param extras The extras.
*/
@Override
- public void onExtrasChanged(Call call, int source, Bundle extras) {
- // Do not inform InCallServices of changes which originated there.
+ public void onExtrasChanged(Call call, int source, Bundle extras,
+ String requestingPackageName) {
+ // Do not inform of changes which originated from an InCallService to a CDS.
if (source == Call.SOURCE_INCALL_SERVICE) {
return;
}
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
new file mode 100644
index 000000000..7e11b47f4
--- /dev/null
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 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 android.content.Context;
+import android.bluetooth.BluetoothDevice;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
+import android.telecom.CallEndpointException;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides to {@link CallsManager} the service that can request change of CallEndpoint to the
+ * {@link CallAudioManager}. And notify change of CallEndpoint status to {@link CallsManager}
+ */
+public class CallEndpointController extends CallsManagerListenerBase {
+ public static final int CHANGE_TIMEOUT_SEC = 2;
+ public static final int RESULT_REQUEST_SUCCESS = 0;
+ public static final int RESULT_ENDPOINT_DOES_NOT_EXIST = 1;
+ public static final int RESULT_REQUEST_TIME_OUT = 2;
+ public static final int RESULT_ANOTHER_REQUEST = 3;
+ public static final int RESULT_UNSPECIFIED_ERROR = 4;
+
+ private final Context mContext;
+ private final CallsManager mCallsManager;
+ private final HashMap<Integer, Integer> mRouteToTypeMap;
+ private final HashMap<Integer, Integer> mTypeToRouteMap;
+ private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
+ private final Set<CallEndpoint> mAvailableCallEndpoints = new HashSet<>();
+ private CallEndpoint mActiveCallEndpoint;
+ private ParcelUuid mRequestedEndpointId;
+ private CompletableFuture<Integer> mPendingChangeRequest;
+
+ public CallEndpointController(Context context, CallsManager callsManager) {
+ mContext = context;
+ mCallsManager = callsManager;
+
+ mRouteToTypeMap = new HashMap<>(5);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_WIRED_HEADSET, CallEndpoint.TYPE_WIRED_HEADSET);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_SPEAKER, CallEndpoint.TYPE_SPEAKER);
+ mRouteToTypeMap.put(CallAudioState.ROUTE_STREAMING, CallEndpoint.TYPE_STREAMING);
+
+ mTypeToRouteMap = new HashMap<>(5);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_BLUETOOTH, CallAudioState.ROUTE_BLUETOOTH);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
+ mTypeToRouteMap.put(CallEndpoint.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
+ }
+
+ @VisibleForTesting
+ public CallEndpoint getCurrentCallEndpoint() {
+ return mActiveCallEndpoint;
+ }
+
+ @VisibleForTesting
+ public Set<CallEndpoint> getAvailableEndpoints() {
+ return mAvailableCallEndpoints;
+ }
+
+ public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+ Log.d(this, "requestCallEndpointChange %s", endpoint);
+ int route = mTypeToRouteMap.get(endpoint.getEndpointType());
+ String bluetoothAddress = getBluetoothAddress(endpoint);
+
+ if (findMatchingTypeEndpoint(endpoint.getEndpointType()) == null ||
+ (route == CallAudioState.ROUTE_BLUETOOTH && bluetoothAddress == null)) {
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED,
+ getErrorResult(RESULT_ENDPOINT_DOES_NOT_EXIST));
+ return;
+ }
+
+ if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) {
+ Log.d(this, "requestCallEndpointChange: requested endpoint is already active");
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
+ return;
+ }
+
+ if (mPendingChangeRequest != null && !mPendingChangeRequest.isDone()) {
+ mPendingChangeRequest.complete(RESULT_ANOTHER_REQUEST);
+ mPendingChangeRequest = null;
+ mRequestedEndpointId = null;
+ }
+
+ mPendingChangeRequest = new CompletableFuture<Integer>()
+ .completeOnTimeout(RESULT_REQUEST_TIME_OUT, CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS);
+
+ mPendingChangeRequest.thenAcceptAsync((result) -> {
+ if (result == RESULT_REQUEST_SUCCESS) {
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
+ } else {
+ callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, getErrorResult(result));
+ }
+ });
+ mRequestedEndpointId = endpoint.getIdentifier();
+ mCallsManager.getCallAudioManager().setAudioRoute(route, bluetoothAddress);
+ }
+
+ public boolean isCurrentEndpointRequestedEndpoint(int requestedRoute, String requestedAddress) {
+ if (mCallsManager.getCallAudioManager() == null
+ || mCallsManager.getCallAudioManager().getCallAudioState() == null) {
+ 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;
+ }
+ return false;
+ }
+
+ private Bundle getErrorResult(int result) {
+ String message;
+ int resultCode;
+ switch (result) {
+ case RESULT_ENDPOINT_DOES_NOT_EXIST:
+ message = "Requested CallEndpoint does not exist";
+ resultCode = CallEndpointException.ERROR_ENDPOINT_DOES_NOT_EXIST;
+ break;
+ case RESULT_REQUEST_TIME_OUT:
+ message = "The operation was not completed on time";
+ resultCode = CallEndpointException.ERROR_REQUEST_TIME_OUT;
+ break;
+ case RESULT_ANOTHER_REQUEST:
+ message = "The operation was canceled by another request";
+ resultCode = CallEndpointException.ERROR_ANOTHER_REQUEST;
+ break;
+ default:
+ message = "The operation has failed due to an unknown or unspecified error";
+ resultCode = CallEndpointException.ERROR_UNSPECIFIED;
+ }
+ CallEndpointException exception = new CallEndpointException(message, resultCode);
+ Bundle extras = new Bundle();
+ extras.putParcelable(CallEndpointException.CHANGE_ERROR, exception);
+ return extras;
+ }
+
+ @VisibleForTesting
+ public String getBluetoothAddress(CallEndpoint endpoint) {
+ return mBluetoothAddressMap.get(endpoint.getIdentifier());
+ }
+
+ private void notifyCallEndpointChange() {
+ if (mActiveCallEndpoint == null) {
+ Log.i(this, "notifyCallEndpointChange, invalid CallEndpoint");
+ return;
+ }
+
+ if (mRequestedEndpointId != null && mPendingChangeRequest != null &&
+ mRequestedEndpointId.equals(mActiveCallEndpoint.getIdentifier())) {
+ mPendingChangeRequest.complete(RESULT_REQUEST_SUCCESS);
+ mPendingChangeRequest = null;
+ mRequestedEndpointId = null;
+ }
+ mCallsManager.updateCallEndpoint(mActiveCallEndpoint);
+
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper()
+ .onCallEndpointChanged(call, mActiveCallEndpoint);
+ }
+ }
+ }
+
+ private void notifyAvailableCallEndpointsChange() {
+ mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
+
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onAvailableCallEndpointsChanged(call,
+ mAvailableCallEndpoints);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper()
+ .onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+ }
+ }
+ }
+
+ private void notifyMuteStateChange(boolean isMuted) {
+ mCallsManager.updateMuteState(isMuted);
+
+ Set<Call> calls = mCallsManager.getTrackedCalls();
+ for (Call call : calls) {
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onMuteStateChanged(call, isMuted);
+ } else if (call != null && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+ }
+ }
+ }
+
+ private void createAvailableCallEndpoints(CallAudioState state) {
+ Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
+ Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
+
+ mRouteToTypeMap.forEach((route, type) -> {
+ if ((state.getSupportedRouteMask() & route) != 0) {
+ if (type == CallEndpoint.TYPE_STREAMING) {
+ if (state.getRoute() == CallAudioState.ROUTE_STREAMING) {
+ if (mActiveCallEndpoint == null
+ || mActiveCallEndpoint.getEndpointType() != type) {
+ mActiveCallEndpoint = new CallEndpoint(getEndpointName(type) != null
+ ? getEndpointName(type) : "", type);
+ }
+ }
+ } else if (type == CallEndpoint.TYPE_BLUETOOTH) {
+ for (BluetoothDevice device : state.getSupportedBluetoothDevices()) {
+ CallEndpoint endpoint = findMatchingBluetoothEndpoint(device);
+ if (endpoint == null) {
+ String deviceName = device.getName();
+ endpoint = new CallEndpoint(
+ deviceName != null ? deviceName : "",
+ CallEndpoint.TYPE_BLUETOOTH);
+ }
+ newAvailableEndpoints.add(endpoint);
+ newBluetoothDevices.put(endpoint.getIdentifier(), device.getAddress());
+
+ BluetoothDevice activeDevice = state.getActiveBluetoothDevice();
+ if (state.getRoute() == route && device.equals(activeDevice)) {
+ mActiveCallEndpoint = endpoint;
+ }
+ }
+ } else {
+ CallEndpoint endpoint = findMatchingTypeEndpoint(type);
+ if (endpoint == null) {
+ endpoint = new CallEndpoint(
+ getEndpointName(type) != null ? getEndpointName(type) : "", type);
+ }
+ newAvailableEndpoints.add(endpoint);
+ if (state.getRoute() == route) {
+ mActiveCallEndpoint = endpoint;
+ }
+ }
+ }
+ });
+ mAvailableCallEndpoints.clear();
+ mAvailableCallEndpoints.addAll(newAvailableEndpoints);
+ mBluetoothAddressMap.clear();
+ mBluetoothAddressMap.putAll(newBluetoothDevices);
+ }
+
+ private CallEndpoint findMatchingTypeEndpoint(int targetType) {
+ for (CallEndpoint endpoint : mAvailableCallEndpoints) {
+ if (endpoint.getEndpointType() == targetType) {
+ return endpoint;
+ }
+ }
+ return null;
+ }
+
+ private CallEndpoint findMatchingBluetoothEndpoint(BluetoothDevice device) {
+ final String targetAddress = device.getAddress();
+ if (targetAddress != null) {
+ for (CallEndpoint endpoint : mAvailableCallEndpoints) {
+ final String address = mBluetoothAddressMap.get(endpoint.getIdentifier());
+ if (targetAddress.equals(address)) {
+ return endpoint;
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState) {
+ if (oldState == null) {
+ return true;
+ }
+ if ((oldState.getSupportedRouteMask() ^ newState.getSupportedRouteMask()) != 0) {
+ return true;
+ }
+ if (oldState.getSupportedBluetoothDevices().size() !=
+ newState.getSupportedBluetoothDevices().size()) {
+ return true;
+ }
+ for (BluetoothDevice device : newState.getSupportedBluetoothDevices()) {
+ if (!oldState.getSupportedBluetoothDevices().contains(device)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isEndpointChanged(CallAudioState oldState, CallAudioState newState) {
+ if (oldState == null) {
+ return true;
+ }
+ if (oldState.getRoute() != newState.getRoute()) {
+ return true;
+ }
+ if (newState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
+ if (oldState.getActiveBluetoothDevice() == null) {
+ if (newState.getActiveBluetoothDevice() == null) {
+ return false;
+ }
+ return true;
+ }
+ return !oldState.getActiveBluetoothDevice().equals(newState.getActiveBluetoothDevice());
+ }
+ return false;
+ }
+
+ private boolean isMuteStateChanged(CallAudioState oldState, CallAudioState newState) {
+ if (oldState == null) {
+ return true;
+ }
+ return oldState.isMuted() != newState.isMuted();
+ }
+
+ private CharSequence getEndpointName(int endpointType) {
+ switch (endpointType) {
+ case CallEndpoint.TYPE_EARPIECE:
+ return mContext.getText(R.string.callendpoint_name_earpiece);
+ case CallEndpoint.TYPE_BLUETOOTH:
+ return mContext.getText(R.string.callendpoint_name_bluetooth);
+ case CallEndpoint.TYPE_WIRED_HEADSET:
+ return mContext.getText(R.string.callendpoint_name_wiredheadset);
+ case CallEndpoint.TYPE_SPEAKER:
+ return mContext.getText(R.string.callendpoint_name_speaker);
+ case CallEndpoint.TYPE_STREAMING:
+ return mContext.getText(R.string.callendpoint_name_streaming);
+ default:
+ return mContext.getText(R.string.callendpoint_name_unknown);
+ }
+ }
+
+ @Override
+ public void onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState) {
+ Log.i(this, "onCallAudioStateChanged, audioState: %s -> %s", oldState, newState);
+
+ if (newState == null) {
+ Log.i(this, "onCallAudioStateChanged, invalid audioState");
+ return;
+ }
+
+ createAvailableCallEndpoints(newState);
+
+ boolean isforce = true;
+ if (isAvailableEndpointChanged(oldState, newState)) {
+ notifyAvailableCallEndpointsChange();
+ isforce = false;
+ }
+
+ if (isEndpointChanged(oldState, newState)) {
+ notifyCallEndpointChange();
+ isforce = false;
+ }
+
+ if (isMuteStateChanged(oldState, newState)) {
+ notifyMuteStateChange(newState.isMuted());
+ isforce = false;
+ }
+
+ if (isforce) {
+ notifyAvailableCallEndpointsChange();
+ notifyCallEndpointChange();
+ notifyMuteStateChange(newState.isMuted());
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/server/telecom/CallEndpointControllerFactory.java b/src/com/android/server/telecom/CallEndpointControllerFactory.java
new file mode 100644
index 000000000..a9b03c349
--- /dev/null
+++ b/src/com/android/server/telecom/CallEndpointControllerFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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 android.content.Context;
+
+/**
+ * Abstracts out creation of CallEndpointController for unit test purposes.
+ */
+public interface CallEndpointControllerFactory {
+ CallEndpointController create(Context context, TelecomSystem.SyncRoot lock,
+ CallsManager callsManager);
+} \ No newline at end of file
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index 7f864b850..7953324df 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -182,9 +182,10 @@ public class CallIntentProcessor {
boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
initiatingUser.getIdentifier());
+
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
- isPrivilegedDialer, defaultDialerCache);
+ isPrivilegedDialer, defaultDialerCache, new MmiUtils());
// If the broadcaster comes back with an immediate error, disconnect and show a dialog.
NewOutgoingCallIntentBroadcaster.CallDisposition disposition = broadcaster.evaluateCall();
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 0ec236274..3005656b3 100755..100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -324,7 +324,7 @@ public final class CallLogManager extends CallsManagerListenerBase {
}
PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(accountHandle);
- UserHandle initiatingUser = call.getInitiatingUser();
+ UserHandle initiatingUser = call.getAssociatedUser();
if (phoneAccount != null &&
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
if (initiatingUser != null &&
@@ -386,7 +386,12 @@ public final class CallLogManager extends CallsManagerListenerBase {
if (okayToLog) {
AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(),
logCallCompletedListener);
+ Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber)
+ + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres="
+ + call.getHandlePresentation());
logCallAsync(args);
+ } else {
+ Log.addEvent(call, LogUtils.Events.SKIP_CALL_LOG);
}
}
diff --git a/src/com/android/server/telecom/CallScreeningServiceHelper.java b/src/com/android/server/telecom/CallScreeningServiceHelper.java
index 0168590ff..942610002 100644
--- a/src/com/android/server/telecom/CallScreeningServiceHelper.java
+++ b/src/com/android/server/telecom/CallScreeningServiceHelper.java
@@ -229,8 +229,9 @@ public class CallScreeningServiceHelper {
serviceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_SCHEDULE_LIKE_TOP_APP,
- UserHandle.CURRENT)) {
- Log.d(TAG, "bindService, found service, waiting for it to connect");
+ userHandle)) {
+ Log.d(TAG,"bindServiceAsUser, found service,"
+ + "waiting for it to connect to user: %s", userHandle);
return true;
}
diff --git a/src/com/android/server/telecom/CallState.java b/src/com/android/server/telecom/CallState.java
index 0411ecc8d..c35c7ecfe 100644
--- a/src/com/android/server/telecom/CallState.java
+++ b/src/com/android/server/telecom/CallState.java
@@ -132,6 +132,46 @@ public final class CallState {
*/
public static final int SIMULATED_RINGING = TelecomProtoEnums.SIMULATED_RINGING; // = 13
+ /**
+ * Determines if a given call state is "transitory". A transitory call state is one which a
+ * call should only be in for a short duration of time before lower levels move it to a more
+ * permanent stable state.
+ *
+ * It is tempting to consider {@link #DIALING}, for example, to be transitory, however the time
+ * spent in this state is entirely dependent on how long the call is actually in that state and
+ * it is expected a call can be {@link #DIALING} for potentially a minute or more.
+ * @param callState the state to check
+ * @return {@code true} if the state is transitory, {@code false} otherwise.
+ */
+ public static boolean isTransitoryState(int callState) {
+ switch (callState) {
+ case NEW:
+ case CONNECTING:
+ case DISCONNECTING:
+ case ANSWERED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Determines if a given call state is "intermediate". An intermediate call state may exist
+ * before a stable call state, but may be longer than a transitory state.
+ *
+ * @param callState the state to check
+ * @return {@code true} if the state is intermediate, {@code false} otherwise.
+ */
+ public static boolean isIntermediateState(int callState) {
+ switch (callState) {
+ case DIALING:
+ case RINGING:
+ return true;
+ default:
+ return false;
+ }
+ }
+
public static String toString(int callState) {
switch (callState) {
case NEW:
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
new file mode 100644
index 000000000..1323633fc
--- /dev/null
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -0,0 +1,443 @@
+/*
+ * 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 android.telecom.CallStreamingService.STREAMING_FAILED_SENDER_BINDING_ERROR;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.role.RoleManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.CallException;
+import android.telecom.CallStreamingService;
+import android.telecom.StreamingCall;
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class CallStreamingController extends CallsManagerListenerBase {
+ private Call mStreamingCall;
+ private TransactionalServiceWrapper mTransactionalServiceWrapper;
+ private ICallStreamingService mService;
+ private final Context mContext;
+ private CallStreamingServiceConnection mConnection;
+ private boolean mIsStreaming;
+ private final Object mLock;
+ private TelecomSystem.SyncRoot mTelecomLock;
+
+ public CallStreamingController(Context context, TelecomSystem.SyncRoot telecomLock) {
+ mLock = new Object();
+ mContext = context;
+ mTelecomLock = telecomLock;
+ }
+
+ private void onConnectedInternal(Call call, TransactionalServiceWrapper wrapper,
+ IBinder service) throws RemoteException {
+ synchronized (mLock) {
+ Log.i(this, "onConnectedInternal: callid=%s", call.getId());
+ Bundle extras = new Bundle();
+ extras.putString(StreamingCall.EXTRA_CALL_ID, call.getId());
+ mStreamingCall = call;
+ mTransactionalServiceWrapper = wrapper;
+ mService = ICallStreamingService.Stub.asInterface(service);
+ mService.setStreamingCallAdapter(new StreamingCallAdapter(mTransactionalServiceWrapper,
+ mStreamingCall,
+ mStreamingCall.getTargetPhoneAccount().getComponentName().getPackageName()));
+ mService.onCallStreamingStarted(new StreamingCall(
+ mTransactionalServiceWrapper.getComponentName(),
+ mStreamingCall.getCallerDisplayName(),
+ mStreamingCall.getHandle(), extras));
+ mIsStreaming = true;
+ }
+ }
+
+ private void resetController() {
+ synchronized (mLock) {
+ mStreamingCall = null;
+ mTransactionalServiceWrapper = null;
+ if (mConnection != null) {
+ // Notify service streaming stopped and then unbind.
+ try {
+ mService.onCallStreamingStopped();
+ } catch (RemoteException e) {
+ // Could not notify stop streaming; we're about to just unbind so this is
+ // unfortunate but not the end of the world.
+ Log.e(this, e, "resetController: failed to notify stop streaming.");
+ }
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ mService = null;
+ mIsStreaming = false;
+ }
+ }
+
+ public boolean isStreaming() {
+ synchronized (mLock) {
+ return mIsStreaming;
+ }
+ }
+
+ public static class QueryCallStreamingTransaction extends VoipCallTransaction {
+ private final CallsManager mCallsManager;
+
+ public QueryCallStreamingTransaction(CallsManager callsManager) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ }
+
+ @Override
+ public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.i(this, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ if (mCallsManager.getCallStreamingController().isStreaming()) {
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ "STREAMING_FAILED_ALREADY_STREAMING"));
+ } else {
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ }
+
+ return future;
+ }
+ }
+
+ public static class AudioInterceptionTransaction extends VoipCallTransaction {
+ private Call mCall;
+ private boolean mEnterInterception;
+
+ public AudioInterceptionTransaction(Call call, boolean enterInterception,
+ TelecomSystem.SyncRoot lock) {
+ super(lock);
+ mCall = call;
+ mEnterInterception = enterInterception;
+ }
+
+ @Override
+ public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.i(this, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ if (mEnterInterception) {
+ mCall.startStreaming();
+ } else {
+ mCall.stopStreaming();
+ }
+ future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ null));
+ return future;
+ }
+ }
+
+ public StreamingServiceTransaction getCallStreamingServiceTransaction(Context context,
+ TransactionalServiceWrapper wrapper, Call call) {
+ return new StreamingServiceTransaction(context, wrapper, call);
+ }
+
+ public class StreamingServiceTransaction extends VoipCallTransaction {
+ public static final String MESSAGE = "STREAMING_FAILED_NO_SENDER";
+ private final TransactionalServiceWrapper mWrapper;
+ private final Context mContext;
+ private final UserHandle mUserHandle;
+ private final Call mCall;
+
+ public StreamingServiceTransaction(Context context, TransactionalServiceWrapper wrapper,
+ Call call) {
+ super(mTelecomLock);
+ mWrapper = wrapper;
+ mCall = call;
+ mUserHandle = mCall.getAssociatedUser();
+ mContext = context;
+ }
+
+ @SuppressLint("LongLogTag")
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.i(this, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> 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));
+ return future;
+ }
+
+ List<String> holders = roleManager.getRoleHoldersAsUser(
+ 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));
+ return future;
+ }
+ Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
+ Intent serviceIntent = new Intent(CallStreamingService.SERVICE_INTERFACE);
+ serviceIntent.setPackage(holders.get(0));
+ List<ResolveInfo> infos = packageManager.queryIntentServicesAsUser(serviceIntent,
+ 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));
+ return future;
+ }
+
+ ServiceInfo serviceInfo = infos.get(0).serviceInfo;
+
+ if (serviceInfo.permission == null || !serviceInfo.permission.equals(
+ 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));
+ return future;
+ }
+ Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
+ intent.setComponent(serviceInfo.getComponentName());
+
+ mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
+ if (!mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE
+ | 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,
+ "STREAMING_FAILED_SENDER_BINDING_ERROR"));
+ }
+ return future;
+ }
+ }
+
+ public UnbindStreamingServiceTransaction getUnbindStreamingServiceTransaction() {
+ return new UnbindStreamingServiceTransaction();
+ }
+
+ public class UnbindStreamingServiceTransaction extends VoipCallTransaction {
+ public UnbindStreamingServiceTransaction() {
+ super(mTelecomLock);
+ }
+
+ @SuppressLint("LongLogTag")
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.i(this, "processTransaction (unbindStreaming");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ resetController();
+ future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ null));
+ return future;
+ }
+ }
+
+ public class StartStreamingTransaction extends SerialTransaction {
+ private Call mCall;
+
+ public StartStreamingTransaction(List<VoipCallTransaction> subTransactions, Call call,
+ TelecomSystem.SyncRoot lock) {
+ super(subTransactions, lock);
+ mCall = call;
+ }
+
+ @Override
+ public void handleTransactionFailure() {
+ mTransactionalServiceWrapper.stopCallStreaming(mCall);
+ }
+ }
+
+ public VoipCallTransaction 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
+ // 2. change audio mode
+ // 3. bind to EXO
+ // If bind to EXO failed, add transaction for stop the streaming
+
+ // create list for multiple transactions
+ List<VoipCallTransaction> transactions = new ArrayList<>();
+ transactions.add(new QueryCallStreamingTransaction(callsManager));
+ transactions.add(new AudioInterceptionTransaction(call, true, lock));
+ transactions.add(getCallStreamingServiceTransaction(
+ callsManager.getContext(), wrapper, call));
+ return new StartStreamingTransaction(transactions, call, lock);
+ }
+
+ public VoipCallTransaction getStopStreamingTransaction(Call call, TelecomSystem.SyncRoot lock) {
+ // TODO: implement this
+ // Stop streaming transaction flow:
+ List<VoipCallTransaction> transactions = new ArrayList<>();
+
+ // 1. unbind to call streaming service
+ transactions.add(getUnbindStreamingServiceTransaction());
+ // 2. audio route operations
+ transactions.add(new CallStreamingController.AudioInterceptionTransaction(call,
+ false, lock));
+ return new ParallelTransaction(transactions, lock);
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (mStreamingCall == call) {
+ mTransactionalServiceWrapper.stopCallStreaming(call);
+ }
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, int oldState, int newState) {
+ // TODO: make sure we are only able to stream the one call and not switch focus to another
+ // and have it streamed too
+ if (mStreamingCall == call && oldState != newState) {
+ CallStreamingStateChangeTransaction transaction = null;
+ switch (newState) {
+ case CallState.ACTIVE:
+ transaction = new CallStreamingStateChangeTransaction(
+ StreamingCall.STATE_STREAMING);
+ break;
+ case CallState.ON_HOLD:
+ transaction = new CallStreamingStateChangeTransaction(
+ StreamingCall.STATE_HOLDING);
+ break;
+ case CallState.DISCONNECTING:
+ case CallState.DISCONNECTED:
+ Log.addEvent(call, LogUtils.Events.STOP_STREAMING);
+ transaction = new CallStreamingStateChangeTransaction(
+ StreamingCall.STATE_DISCONNECTED);
+ break;
+ default:
+ // ignore
+ }
+ if (transaction != null) {
+ mTransactionalServiceWrapper.getTransactionManager().addTransaction(transaction,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ // ignore
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.e(this, exception, "Exception when set call "
+ + "streaming state to streaming app");
+ }
+ });
+ }
+ }
+ }
+
+ private class CallStreamingStateChangeTransaction extends VoipCallTransaction {
+ @StreamingCall.StreamingCallState int mState;
+
+ public CallStreamingStateChangeTransaction(@StreamingCall.StreamingCallState int state) {
+ super(mTelecomLock);
+ mState = state;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ try {
+ mService.onCallStreamingStateChanged(mState);
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ } catch (RemoteException e) {
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED, "Exception when request "
+ + "setting state to streaming app."));
+ }
+ return future;
+ }
+ }
+
+ private class CallStreamingServiceConnection implements
+ ServiceConnection {
+ private Call mCall;
+ private TransactionalServiceWrapper mWrapper;
+ private CompletableFuture<VoipCallTransactionResult> mFuture;
+
+ public CallStreamingServiceConnection(Call call, TransactionalServiceWrapper wrapper,
+ CompletableFuture<VoipCallTransactionResult> future) {
+ mCall = call;
+ mWrapper = wrapper;
+ mFuture = future;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ try {
+ Log.i(this, "onServiceConnected: " + name);
+ onConnectedInternal(mCall, mWrapper, service);
+ mFuture.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ } catch (RemoteException e) {
+ resetController();
+ mFuture.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ StreamingServiceTransaction.MESSAGE));
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ clearBinding();
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ clearBinding();
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ clearBinding();
+ }
+
+ private void clearBinding() {
+ resetController();
+ if (!mFuture.isDone()) {
+ mFuture.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ "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 3fce79956..77570c3c9 100755..100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -16,9 +16,15 @@
package com.android.server.telecom;
+import static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
+import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
+import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
import static android.provider.CallLog.Calls.SHORT_RING_THRESHOLD;
+import static android.provider.CallLog.Calls.USER_MISSED_CALL_FILTERS_TIMEOUT;
+import static android.provider.CallLog.Calls.USER_MISSED_CALL_SCREENING_SERVICE_SILENCED;
import static android.provider.CallLog.Calls.USER_MISSED_NEVER_RANG;
+import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
import static android.provider.CallLog.Calls.USER_MISSED_NO_ANSWER;
import static android.provider.CallLog.Calls.USER_MISSED_SHORT_RING;
import static android.telecom.TelecomManager.ACTION_POST_CALL;
@@ -32,17 +38,15 @@ import static android.telecom.TelecomManager.EXTRA_HANDLE;
import static android.telecom.TelecomManager.MEDIUM_CALL_TIME_MS;
import static android.telecom.TelecomManager.SHORT_CALL_TIME_MS;
import static android.telecom.TelecomManager.VERY_SHORT_CALL_TIME_MS;
-import static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
-import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
-import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
-import static android.provider.CallLog.Calls.USER_MISSED_CALL_FILTERS_TIMEOUT;
-import static android.provider.CallLog.Calls.USER_MISSED_CALL_SCREENING_SERVICE_SILENCED;
import android.Manifest;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
+import android.app.NotificationManager;
+import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -50,6 +54,8 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ResolveInfoFlags;
+import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
@@ -58,13 +64,14 @@ import android.media.AudioSystem;
import android.media.MediaPlayer;
import android.media.ToneGenerator;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.PersistableBundle;
import android.os.Process;
+import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemVibrator;
import android.os.Trace;
@@ -75,7 +82,10 @@ import android.provider.BlockedNumberContract.SystemContract;
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;
+import android.telecom.CallException;
import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
import android.telecom.Conference;
@@ -93,7 +103,9 @@ import android.telecom.PhoneAccountSuggestion;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.CarrierConfigManager;
+import android.telephony.CellIdentity;
import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Pair;
@@ -103,27 +115,33 @@ import android.view.WindowManager;
import android.widget.Button;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IntentForwarderActivity;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
import com.android.server.telecom.callfiltering.BlockCheckerFilter;
+import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
import com.android.server.telecom.callfiltering.CallFilterResultCallback;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.callfiltering.CallFilteringResult.Builder;
import com.android.server.telecom.callfiltering.CallScreeningServiceFilter;
import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
+import com.android.server.telecom.callfiltering.DndCallFilter;
import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
import com.android.server.telecom.callredirection.CallRedirectionProcessor;
import com.android.server.telecom.components.ErrorDialogActivity;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
-import com.android.server.telecom.settings.BlockedNumbersUtil;
+import com.android.server.telecom.stats.CallFailureCause;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
+import com.android.server.telecom.ui.CallStreamingNotification;
import com.android.server.telecom.ui.ConfirmCallDialogActivity;
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 java.util.ArrayList;
import java.util.Arrays;
@@ -138,10 +156,12 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -155,14 +175,20 @@ import java.util.stream.Stream;
* access from other packages specifically refraining from passing the CallsManager instance
* beyond the com.android.server.telecom package boundary.
*/
-@VisibleForTesting
public class CallsManager extends Call.ListenerBase
implements VideoProviderProxy.Listener, CallFilterResultCallback, CurrentUserProxy {
// TODO: Consider renaming this CallsManagerPlugin.
@VisibleForTesting
public interface CallsManagerListener {
+ /**
+ * Informs listeners when a {@link Call} is newly created, but not yet returned by a
+ * {@link android.telecom.ConnectionService} implementation.
+ * @param call the call.
+ */
+ default void onStartCreateConnection(Call call) {}
void onCallAdded(Call call);
+ void onCreateConnectionFailed(Call call);
void onCallRemoved(Call call);
void onCallStateChanged(Call call, int oldState, int newState);
void onConnectionServiceChanged(
@@ -172,6 +198,9 @@ public class CallsManager extends Call.ListenerBase
void onIncomingCallAnswered(Call call);
void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
+ void onCallEndpointChanged(CallEndpoint callEndpoint);
+ void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints);
+ void onMuteStateChanged(boolean isMuted);
void onRingbackRequested(Call call, boolean ringback);
void onIsConferencedChanged(Call call);
void onIsVoipAudioModeChanged(Call call);
@@ -180,6 +209,7 @@ public class CallsManager extends Call.ListenerBase
void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
void onHoldToneRequested(Call call);
void onExternalCallChanged(Call call, boolean isExternalCall);
+ void onCallStreamingStateChanged(Call call, boolean isStreaming);
void onDisconnectedTonePlaying(boolean isTonePlaying);
void onConnectionTimeChanged(Call call);
void onConferenceStateChanged(Call call, boolean isConference);
@@ -227,6 +257,45 @@ public class CallsManager extends Call.ListenerBase
private static final int MAXIMUM_TOP_LEVEL_CALLS = 2;
private static final int MAXIMUM_SELF_MANAGED_CALLS = 10;
+ /**
+ * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+ */
+ public static final UUID LIVE_CALL_STUCK_CONNECTING_ERROR_UUID =
+ UUID.fromString("3f95808c-9134-11ed-a1eb-0242ac120002");
+ public static final String LIVE_CALL_STUCK_CONNECTING_ERROR_MSG =
+ "Force disconnected a live call that was stuck in CONNECTING state.";
+ public static final UUID LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID =
+ UUID.fromString("744fdf86-9137-11ed-a1eb-0242ac120002");
+ public static final String LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG =
+ "Found a live call that was stuck in CONNECTING state while attempting to place an "
+ + "emergency call.";
+ public static final UUID CALL_REMOVAL_EXECUTION_ERROR_UUID =
+ UUID.fromString("030b8b16-9139-11ed-a1eb-0242ac120002");
+ public static final String CALL_REMOVAL_EXECUTION_ERROR_MSG =
+ "Exception thrown while executing call removal";
+ public static final UUID EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_UUID =
+ UUID.fromString("1c4eed7c-9132-11ed-a1eb-0242ac120002");
+ public static final String EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_MSG =
+ "Exception thrown while establishing connection.";
+ public static final UUID EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_UUID =
+ UUID.fromString("b68c881d-0ed8-4f31-9342-8bf416c96d18");
+ public static final String EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_MSG =
+ "Exception thrown while retrieving list of potential phone accounts.";
+ public static final UUID EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_UUID =
+ UUID.fromString("f272f89d-fb3a-4004-aa2d-20b8d679467e");
+ 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.";
+
private static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.PULLING};
@@ -284,6 +353,16 @@ public class CallsManager extends Call.ListenerBase
new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
/**
+ * List of self-managed calls that have been initialized but not yet added to
+ * CallsManager#addCall(Call). There is a window of time when a Call has been added to Telecom
+ * (e.g. TelecomManager#addNewIncomingCall) to actually added in CallsManager#addCall(Call).
+ * This list is helpful for the NotificationManagerService to know that Telecom is currently
+ * setting up a call which is an important set in making notifications non-dismissible.
+ */
+ private final Set<Call> mSelfManagedCallsBeingSetup = Collections.newSetFromMap(
+ new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
+
+ /**
* A pending call is one which requires user-intervention in order to be placed.
* Used by {@link #startCallConfirmation}.
*/
@@ -374,6 +453,16 @@ public class CallsManager extends Call.ListenerBase
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final EmergencyCallHelper mEmergencyCallHelper;
private final RoleManagerAdapter mRoleManagerAdapter;
+ private final VoipCallMonitor mVoipCallMonitor;
+ private final CallEndpointController mCallEndpointController;
+ private final CallAnomalyWatchdog mCallAnomalyWatchdog;
+
+ private final EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
+ private final CallStreamingController mCallStreamingController;
+ private final BlockedNumbersAdapter mBlockedNumbersAdapter;
+ private final TransactionManager mTransactionManager;
+ private final UserManager mUserManager;
+ private final CallStreamingNotification mCallStreamingNotification;
private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -394,14 +483,18 @@ public class CallsManager extends Call.ListenerBase
private boolean mCanAddCall = true;
- private int mMaxNumberOfSimultaneouslyActiveSims = -1;
-
private Runnable mStopTone;
private LinkedList<HandlerThread> mGraphHandlerThreads;
+ // An executor that can be used to fire off async tasks that do not block Telecom in any manner.
+ private final Executor mAsyncTaskExecutor;
+
private boolean mHasActiveRttCall = false;
+ private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+
+ private final MmiUtils mMmiUtils = new MmiUtils();
/**
* Listener to PhoneAccountRegistrar events.
*/
@@ -432,34 +525,14 @@ public class CallsManager extends Call.ListenerBase
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- Log.startSession("CM.CCCR");
String action = intent.getAction();
if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
|| SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
- new UpdateEmergencyCallNotificationTask().doInBackground(
- Pair.create(context, Log.createSubsession()));
+ updateEmergencyCallNotificationAsync(context);
}
}
};
- private static class UpdateEmergencyCallNotificationTask
- extends AsyncTask<Pair<Context, Session>, Void, Void> {
- @SafeVarargs
- @Override
- protected final Void doInBackground(Pair<Context, Session>... args) {
- if (args == null || args.length != 1 || args[0] == null) {
- Log.e(this, new IllegalArgumentException(), "Incorrect invocation");
- return null;
- }
- Log.continueSession(args[0].second, "CM.UECNT");
- Context context = args[0].first;
- BlockedNumbersUtil.updateEmergencyCallNotification(context,
- SystemContract.shouldShowEmergencyCallNotification(context));
- Log.endSession();
- return null;
- }
- }
-
/**
* Initializes the required Telecom components.
*/
@@ -494,7 +567,16 @@ public class CallsManager extends Call.ListenerBase
InCallControllerFactory inCallControllerFactory,
CallDiagnosticServiceController callDiagnosticServiceController,
RoleManagerAdapter roleManagerAdapter,
- ToastFactory toastFactory) {
+ ToastFactory toastFactory,
+ CallEndpointControllerFactory callEndpointControllerFactory,
+ CallAnomalyWatchdog callAnomalyWatchdog,
+ Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
+ Executor asyncTaskExecutor,
+ BlockedNumbersAdapter blockedNumbersAdapter,
+ TransactionManager transactionManager,
+ EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
+ CallStreamingNotification callStreamingNotification) {
+
mContext = context;
mLock = lock;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
@@ -511,6 +593,7 @@ public class CallsManager extends Call.ListenerBase
mTimeoutsAdapter = timeoutsAdapter;
mEmergencyCallHelper = emergencyCallHelper;
mCallerInfoLookupHelper = callerInfoLookupHelper;
+ mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
@@ -522,7 +605,8 @@ public class CallsManager extends Call.ListenerBase
wiredHeadsetManager,
statusBarNotifier,
audioServiceFactory,
- CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT
+ CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+ asyncTaskExecutor
);
callAudioRouteStateMachine.initialize();
@@ -532,7 +616,6 @@ public class CallsManager extends Call.ListenerBase
bluetoothManager,
wiredHeadsetManager,
mDockManager);
-
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
InCallTonePlayer.MediaPlayerFactory mediaPlayerFactory =
(resourceId, attributes) ->
@@ -549,11 +632,14 @@ public class CallsManager extends Call.ListenerBase
mInCallController = inCallControllerFactory.create(context, mLock, this,
systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
emergencyCallHelper);
+ mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this);
mCallDiagnosticServiceController = callDiagnosticServiceController;
mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory);
mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
ringtoneFactory, systemVibrator,
- new Ringer.VibrationEffectProxy(), mInCallController);
+ new Ringer.VibrationEffectProxy(), mInCallController,
+ mContext.getSystemService(NotificationManager.class),
+ accessibilityManagerAdapter);
mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
mTimeoutsAdapter, mLock);
mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
@@ -574,11 +660,17 @@ public class CallsManager extends Call.ListenerBase
mClockProxy = clockProxy;
mToastFactory = toastFactory;
mRoleManagerAdapter = roleManagerAdapter;
+ mTransactionManager = transactionManager;
+ mBlockedNumbersAdapter = blockedNumbersAdapter;
+ mCallStreamingController = new CallStreamingController(mContext, mLock);
+ mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
+ mCallStreamingNotification = callStreamingNotification;
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
mListeners.add(mCallLogManager);
mListeners.add(mInCallController);
+ mListeners.add(mCallEndpointController);
mListeners.add(mCallDiagnosticServiceController);
mListeners.add(mCallAudioManager);
mListeners.add(mCallRecordingTonePlayer);
@@ -587,9 +679,16 @@ public class CallsManager extends Call.ListenerBase
mListeners.add(mHeadsetMediaButton);
mListeners.add(mProximitySensorManager);
mListeners.add(audioProcessingNotification);
+ mListeners.add(callAnomalyWatchdog);
+ mListeners.add(mEmergencyCallDiagnosticLogger);
+ mListeners.add(mCallStreamingController);
// this needs to be after the mCallAudioManager
mListeners.add(mPhoneStateBroadcaster);
+ mListeners.add(mVoipCallMonitor);
+ mListeners.add(mCallStreamingNotification);
+
+ mVoipCallMonitor.startMonitor();
// There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
final UserManager userManager = UserManager.get(mContext);
@@ -600,9 +699,14 @@ public class CallsManager extends Call.ListenerBase
// Register BroadcastReceiver to handle enhanced call blocking feature related event.
IntentFilter intentFilter = new IntentFilter(
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+ intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
intentFilter.addAction(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
mGraphHandlerThreads = new LinkedList<>();
+
+ mCallAnomalyWatchdog = callAnomalyWatchdog;
+ mAsyncTaskExecutor = asyncTaskExecutor;
+ mUserManager = mContext.getSystemService(UserManager.class);
}
public void setIncomingCallNotifier(IncomingCallNotifier incomingCallNotifier) {
@@ -638,9 +742,11 @@ public class CallsManager extends Call.ListenerBase
}
@Override
+ @VisibleForTesting
public void onSuccessfulOutgoingCall(Call call, int callState) {
Log.v(this, "onSuccessfulOutgoingCall, %s", call);
- call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp());
+ call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp(
+ call.getAssociatedUser()));
setCallState(call, callState, "successful outgoing call");
if (!mCalls.contains(call)) {
@@ -659,8 +765,7 @@ public class CallsManager extends Call.ListenerBase
@Override
public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {
- Log.v(this, "onFailedOutgoingCall, call: %s", call);
-
+ Log.i(this, "onFailedOutgoingCall for call %s", call);
markCallAsRemoved(call);
}
@@ -673,12 +778,19 @@ public class CallsManager extends Call.ListenerBase
phoneAccount == null || phoneAccount.getExtras() == null
? new Bundle()
: phoneAccount.getExtras();
+ TelephonyManager telephonyManager = getTelephonyManager();
if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) ||
+ incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL) ||
+ telephonyManager.isInEmergencySmsMode() ||
incomingCall.isSelfManaged() ||
extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
- Log.i(this, "Skipping call filtering for %s (ecm=%b, selfMgd=%b, skipExtra=%b)",
+ 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(),
incomingCall.isSelfManaged(),
extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING));
onCallFilteringComplete(incomingCall, new Builder()
@@ -698,8 +810,11 @@ public class CallsManager extends Call.ListenerBase
private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) {
incomingCall.setIsUsingCallFiltering(true);
String carrierPackageName = getCarrierPackageName();
- String defaultDialerPackageName = TelecomManager.from(mContext).getDefaultDialerPackage();
- String userChosenPackageName = getRoleManagerAdapter().getDefaultCallScreeningApp();
+ UserHandle userHandle = incomingCall.getAssociatedUser();
+ String defaultDialerPackageName = TelecomManager.from(mContext).
+ getDefaultDialerPackage(userHandle);
+ String userChosenPackageName = getRoleManagerAdapter().
+ getDefaultCallScreeningApp(userHandle);
AppLabelProxy appLabelProxy = packageName -> AppLabelProxy.Util.getAppLabel(
mContext.getPackageManager(), packageName);
ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
@@ -710,6 +825,7 @@ public class CallsManager extends Call.ListenerBase
mCallerInfoLookupHelper);
BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
mCallerInfoLookupHelper, new BlockCheckerAdapter());
+ DndCallFilter dndCallFilter = new DndCallFilter(incomingCall, getRinger());
CallScreeningServiceFilter carrierCallScreeningServiceFilter =
new CallScreeningServiceFilter(incomingCall, carrierPackageName,
CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, this,
@@ -727,6 +843,7 @@ public class CallsManager extends Call.ListenerBase
mContext, this, appLabelProxy, converter);
}
graph.addFilter(voicemailFilter);
+ graph.addFilter(dndCallFilter);
graph.addFilter(blockCheckerFilter);
graph.addFilter(carrierCallScreeningServiceFilter);
graph.addFilter(callScreeningServiceFilter);
@@ -774,6 +891,9 @@ public class CallsManager extends Call.ListenerBase
return;
}
+ // Store the shouldSuppress value in the call object which will be passed to InCallServices
+ incomingCall.setCallIsSuppressedByDoNotDisturb(result.shouldSuppressCallDueToDndStatus);
+
// Inform our connection service that call filtering is done (if it was performed at all).
if (incomingCall.isUsingCallFiltering()) {
boolean isInContacts = incomingCall.getCallerInfo() != null
@@ -791,8 +911,7 @@ public class CallsManager extends Call.ListenerBase
}
// Get rid of the call composer attachments that aren't wanted
- if (result.mIsResponseFromSystemDialer && result.mCallScreeningResponse != null
- && result.mCallScreeningResponse.getCallComposerAttachmentsToShow() >= 0) {
+ if (result.mIsResponseFromSystemDialer && result.mCallScreeningResponse != null) {
int attachmentMask = result.mCallScreeningResponse.getCallComposerAttachmentsToShow();
if ((attachmentMask
& CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_LOCATION) == 0) {
@@ -812,7 +931,9 @@ public class CallsManager extends Call.ListenerBase
if (result.shouldAllowCall) {
incomingCall.setPostCallPackageName(
- getRoleManagerAdapter().getDefaultCallScreeningApp());
+ getRoleManagerAdapter().getDefaultCallScreeningApp(
+ incomingCall.getAssociatedUser()
+ ));
Log.i(this, "onCallFilteringComplete: allow call.");
if (hasMaximumManagedRingingCalls(incomingCall)) {
@@ -861,7 +982,8 @@ public class CallsManager extends Call.ListenerBase
}
mCallLogManager.logCall(incomingCall, Calls.BLOCKED_TYPE,
result.shouldShowNotification, result);
- } else if (result.shouldShowNotification) {
+ }
+ if (result.shouldShowNotification) {
Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
mMissedCallNotifier.showMissedCallNotification(
new MissedCallNotifier.CallInfo(incomingCall));
@@ -901,14 +1023,15 @@ public class CallsManager extends Call.ListenerBase
@Override
public void onFailedIncomingCall(Call call) {
+ Log.i(this, "onFailedIncomingCall for call %s", call);
setCallState(call, CallState.DISCONNECTED, "failed incoming call");
call.removeListener(this);
}
@Override
public void onSuccessfulUnknownCall(Call call, int callState) {
- setCallState(call, callState, "successful unknown call");
Log.i(this, "onSuccessfulUnknownCall for call %s", call);
+ setCallState(call, callState, "successful unknown call");
addCall(call);
}
@@ -1128,7 +1251,6 @@ public class CallsManager extends Call.ListenerBase
}
}
- @VisibleForTesting
public Call getForegroundCall() {
if (mCallAudioManager == null) {
// Happens when getForegroundCall is called before full initialization.
@@ -1137,6 +1259,15 @@ public class CallsManager extends Call.ListenerBase
return mCallAudioManager.getForegroundCall();
}
+ @VisibleForTesting
+ public Set<Call> getTrackedCalls() {
+ if (mCallAudioManager == null) {
+ // Happens when getTrackedCalls is called before full initialization.
+ return null;
+ }
+ return mCallAudioManager.getTrackedCalls();
+ }
+
@Override
public void onCallHoldFailed(Call call) {
markAllAnsweredCallAsRinging(call, "hold");
@@ -1172,10 +1303,18 @@ public class CallsManager extends Call.ListenerBase
return mInCallController;
}
+ public CallEndpointController getCallEndpointController() {
+ return mCallEndpointController;
+ }
+
EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
+ EmergencyCallDiagnosticLogger getEmergencyCallDiagnosticLogger() {
+ return mEmergencyCallDiagnosticLogger;
+ }
+
public DefaultDialerCache getDefaultDialerCache() {
return mDefaultDialerCache;
}
@@ -1239,6 +1378,11 @@ public class CallsManager extends Call.ListenerBase
mListeners.remove(listener);
}
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+ mAnomalyReporter = mAnomalyReporterAdapter;
+ }
+
void processIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
Log.d(this, "processIncomingCallConference");
processIncomingCallIntent(phoneAccountHandle, extras, true);
@@ -1255,7 +1399,7 @@ public class CallsManager extends Call.ListenerBase
processIncomingCallIntent(phoneAccountHandle, extras, false);
}
- void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras,
+ public Call processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras,
boolean isConference) {
Log.d(this, "processIncomingCallIntent");
boolean isHandover = extras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER);
@@ -1264,8 +1408,10 @@ public class CallsManager extends Call.ListenerBase
// Required for backwards compatibility
handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
+ PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+ phoneAccountHandle);
Call call = new Call(
- getNextCallId(),
+ generateNextCallId(extras),
mContext,
this,
mLock,
@@ -1280,16 +1426,37 @@ public class CallsManager extends Call.ListenerBase
isConference, /* isConference */
mClockProxy,
mToastFactory);
-
- // Ensure new calls related to self-managed calls/connections are set as such. This will
+ // 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
// from the moment it is created.
- PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
- phoneAccountHandle);
+ boolean isSelfManaged = phoneAccount != null && phoneAccount.isSelfManaged();
+ call.setIsSelfManaged(isSelfManaged);
+ // It's important to start tracking self-managed calls as soon as the Call object is
+ // initialized so NotificationManagerService is aware Telecom is setting up a call
+ if (isSelfManaged) mSelfManagedCallsBeingSetup.add(call);
+
+ // set properties for transactional call
+ 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.setTargetPhoneAccount(phoneAccountHandle);
+ if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+ CharSequence displayName = extras.getCharSequence(CallAttributes.DISPLAY_NAME_KEY);
+ if (!TextUtils.isEmpty(displayName)) {
+ call.setCallerDisplayName(displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
+ // Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above.
+ call.setAssociatedUser(phoneAccountHandle.getUserHandle());
+ }
+
if (phoneAccount != null) {
Bundle phoneAccountExtras = phoneAccount.getExtras();
- call.setIsSelfManaged(phoneAccount.isSelfManaged());
if (call.isSelfManaged()) {
// Self managed calls will always be voip audio mode.
call.setIsVoipAudioMode(true);
@@ -1298,7 +1465,7 @@ public class CallsManager extends Call.ListenerBase
PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true));
} else {
// Incoming call is managed, the active call is self-managed and can't be held.
- // We need to set extras on it to indicate whether answering will cause a
+ // We need to set extras on it to indicate whether answering will cause a
// active self-managed call to drop.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
if (activeCall != null && !canHold(activeCall) && activeCall.isSelfManaged()) {
@@ -1310,7 +1477,7 @@ public class CallsManager extends Call.ListenerBase
dropCallExtras.putCharSequence(
Connection.EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME, droppedApp);
Log.i(this, "Incoming managed call will drop %s call.", droppedApp);
- call.putExtras(Call.SOURCE_CONNECTION_SERVICE, dropCallExtras);
+ call.putConnectionServiceExtras(dropCallExtras);
}
}
@@ -1400,16 +1567,42 @@ public class CallsManager extends Call.ListenerBase
}
}
- if (!isHandoverAllowed || (call.isSelfManaged() && !isIncomingCallPermitted(call,
- call.getTargetPhoneAccount()))) {
+ CallFailureCause startFailCause =
+ checkIncomingCallPermitted(call, call.getTargetPhoneAccount());
+ // 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()) {
+ Log.d(TAG, "Rejecting non-emergency call because the owner %s is not running.",
+ phoneAccountHandle.getUserHandle());
+ call.setMissedReason(USER_MISSED_NOT_RUNNING);
+ call.setStartFailCause(CallFailureCause.INVALID_USE);
+ if (isConference) {
+ notifyCreateConferenceFailed(phoneAccountHandle, call);
+ } else {
+ notifyCreateConnectionFailed(phoneAccountHandle, call);
+ }
+ }
+ else if (!isHandoverAllowed ||
+ (call.isSelfManaged() && !startFailCause.isSuccess())) {
if (isConference) {
notifyCreateConferenceFailed(phoneAccountHandle, call);
} else {
if (hasMaximumManagedRingingCalls(call)) {
call.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
+ call.setStartFailCause(CallFailureCause.MAX_RINGING_CALLS);
mCallLogManager.logCall(call, Calls.MISSED_TYPE,
true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
}
+ call.setStartFailCause(startFailCause);
notifyCreateConnectionFailed(phoneAccountHandle, call);
}
} else if (isInEmergencyCall()) {
@@ -1418,6 +1611,7 @@ public class CallsManager extends Call.ListenerBase
// rejected since the user did not explicitly reject.
call.setMissedReason(AUTO_MISSED_EMERGENCY_CALL);
call.getAnalytics().setMissedReason(call.getMissedReason());
+ call.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
mCallLogManager.logCall(call, Calls.MISSED_TYPE,
true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
if (isConference) {
@@ -1425,9 +1619,18 @@ public class CallsManager extends Call.ListenerBase
} else {
notifyCreateConnectionFailed(phoneAccountHandle, call);
}
+ } else if (call.isTransactionalCall()) {
+ // transactional calls should skip Call#startCreateConnection below
+ // as that is meant for Call objects with a ConnectionServiceWrapper
+ call.setState(CallState.RINGING, "explicitly set new incoming to ringing");
+ // Transactional calls don't get created via a connection service; they are added now.
+ call.setIsCreateConnectionComplete(true);
+ addCall(call);
} else {
+ notifyStartCreateConnection(call);
call.startCreateConnection(mPhoneAccountRegistrar);
}
+ return call;
}
void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -1453,8 +1656,11 @@ public class CallsManager extends Call.ListenerBase
mToastFactory);
call.initAnalytics();
+ // For unknown calls, base the associated user off of the target phone account handle.
+ call.setAssociatedUser(phoneAccountHandle.getUserHandle());
setIntentExtrasAndStartTime(call, extras);
call.addListener(this);
+ notifyStartCreateConnection(call);
call.startCreateConnection(mPhoneAccountRegistrar);
}
@@ -1518,6 +1724,14 @@ public class CallsManager extends Call.ListenerBase
originalIntent, callingPackage, false);
}
+ private String generateNextCallId(Bundle extras) {
+ if (extras != null && extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
+ return extras.getString(TelecomManager.TRANSACTION_CALL_ID_KEY);
+ } else {
+ return getNextCallId();
+ }
+ }
+
private CompletableFuture<Call> startOutgoingCall(List<Uri> participants,
PhoneAccountHandle requestedAccountHandle,
Bundle extras, UserHandle initiatingUser, Intent originalIntent,
@@ -1525,7 +1739,6 @@ public class CallsManager extends Call.ListenerBase
boolean isReusedCall;
Uri handle = isConference ? Uri.parse("tel:conf-factory") : participants.get(0);
Call call = reuseOutgoingCall(handle);
-
PhoneAccount account =
mPhoneAccountRegistrar.getPhoneAccount(requestedAccountHandle, initiatingUser);
Bundle phoneAccountExtra = account != null ? account.getExtras() : null;
@@ -1544,7 +1757,7 @@ public class CallsManager extends Call.ListenerBase
// Create a call with original handle. The handle may be changed when the call is attached
// to a connection service, but in most cases will remain the same.
if (call == null) {
- call = new Call(getNextCallId(), mContext,
+ call = new Call(generateNextCallId(extras), mContext,
this,
mLock,
mConnectionServiceRepository,
@@ -1553,14 +1766,48 @@ public class CallsManager extends Call.ListenerBase
isConference ? participants : null,
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
- null /* requestedAccountHandle */,
+ requestedAccountHandle /* targetPhoneAccountHandle */,
Call.CALL_DIRECTION_OUTGOING /* callDirection */,
false /* forceAttachToExistingConnection */,
isConference, /* isConference */
mClockProxy,
mToastFactory);
+
+ 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);
+ if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+ CharSequence displayName = extras.getCharSequence(
+ CallAttributes.DISPLAY_NAME_KEY);
+ if (!TextUtils.isEmpty(displayName)) {
+ call.setCallerDisplayName(displayName.toString(),
+ TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
+ }
+
call.initAnalytics(callingPackage, creationLogs.toString());
+ // 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,
+ handle.getSchemeSpecificPart(),
+ callingPackage, simNumeric, networkNumeric);
+ }
+
// 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
@@ -1573,11 +1820,14 @@ public class CallsManager extends Call.ListenerBase
|| phoneAccountExtra.getBoolean(
PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true));
}
- call.setInitiatingUser(initiatingUser);
+ call.setAssociatedUser(initiatingUser);
isReusedCall = false;
} else {
isReusedCall = true;
}
+ // It's important to start tracking self-managed calls as soon as the Call object is
+ // initialized so NotificationManagerService is aware Telecom is setting up a call
+ if (isSelfManaged) mSelfManagedCallsBeingSetup.add(call);
int videoState = VideoProfile.STATE_AUDIO_ONLY;
if (extras != null) {
@@ -1631,6 +1881,18 @@ public class CallsManager extends Call.ListenerBase
// retrieved.
CompletableFuture<List<PhoneAccountHandle>> setAccountHandle =
accountsForCall.whenCompleteAsync((potentialPhoneAccounts, exception) -> {
+ if (exception != null){
+ Log.e(TAG, exception, "Error retrieving list of potential phone accounts.");
+ if (finalCall.isEmergencyCall()) {
+ mAnomalyReporter.reportAnomaly(
+ EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_UUID,
+ EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG);
+ } else {
+ mAnomalyReporter.reportAnomaly(
+ EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_UUID,
+ EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_MSG);
+ }
+ }
Log.i(CallsManager.this, "set outgoing call phone acct; potentialAccts=%s",
potentialPhoneAccounts);
PhoneAccountHandle phoneAccountHandle;
@@ -1670,7 +1932,7 @@ public class CallsManager extends Call.ListenerBase
CompletableFuture<Call> makeRoomForCall = setAccountHandle.thenComposeAsync(
potentialPhoneAccounts -> {
Log.i(CallsManager.this, "make room for outgoing call stage");
- if (isPotentialInCallMMICode(handle) && !isSelfManaged) {
+ if (mMmiUtils.isPotentialInCallMMICode(handle) && !isSelfManaged) {
return CompletableFuture.completedFuture(finalCall);
}
// If a call is being reused, then it has already passed the
@@ -1703,6 +1965,7 @@ public class CallsManager extends Call.ListenerBase
notifyCreateConnectionFailed(
finalCall.getTargetPhoneAccount(), finalCall);
}
+ finalCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
return CompletableFuture.completedFuture(null);
}
@@ -1761,9 +2024,34 @@ public class CallsManager extends Call.ListenerBase
return CompletableFuture.completedFuture(null);
}
if (accountSuggestions == null || accountSuggestions.isEmpty()) {
+ Uri callUri = callToPlace.getHandle();
+ if (PhoneAccount.SCHEME_TEL.equals(callUri.getScheme())) {
+ int managedProfileUserId = getManagedProfileUserId(mContext,
+ initiatingUser.getIdentifier());
+ if (managedProfileUserId != UserHandle.USER_NULL
+ &&
+ mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
+ handle.getScheme(), false,
+ UserHandle.of(managedProfileUserId),
+ false).size()
+ != 0) {
+ boolean dialogShown = showSwitchToManagedProfileDialog(
+ callUri, initiatingUser, managedProfileUserId);
+ if (dialogShown) {
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+ }
+
Log.i(CallsManager.this, "Aborting call since there are no"
+ " available accounts.");
showErrorMessage(R.string.cant_call_due_to_no_supported_service);
+ mListeners.forEach(l -> l.onCreateConnectionFailed(callToPlace));
+ if (callToPlace.isEmergencyCall()){
+ mAnomalyReporter.reportAnomaly(
+ EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID,
+ EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG);
+ }
return CompletableFuture.completedFuture(null);
}
boolean needsAccountSelection = accountSuggestions.size() > 1
@@ -1810,6 +2098,8 @@ public class CallsManager extends Call.ListenerBase
dialerSelectPhoneAccountFuture.thenAcceptBothAsync(contactLookupFuture,
(callPhoneAccountHandlePair, uriCallerInfoPair) -> {
Call theCall = callPhoneAccountHandlePair.first;
+ UserHandle userHandleForCallScreening = theCall.
+ getAssociatedUser();
boolean isInContacts = uriCallerInfoPair.second != null
&& uriCallerInfoPair.second.contactExists;
Log.d(CallsManager.this, "outgoingCallIdStage: isInContacts=%s",
@@ -1820,10 +2110,12 @@ public class CallsManager extends Call.ListenerBase
PackageManager packageManager = mContext.getPackageManager();
int permission = packageManager.checkPermission(
Manifest.permission.READ_CONTACTS,
- mRoleManagerAdapter.getDefaultCallScreeningApp());
+ mRoleManagerAdapter.
+ getDefaultCallScreeningApp(userHandleForCallScreening));
Log.d(CallsManager.this,
"default call screening service package %s has permissions=%s",
- mRoleManagerAdapter.getDefaultCallScreeningApp(),
+ mRoleManagerAdapter.
+ getDefaultCallScreeningApp(userHandleForCallScreening),
permission == PackageManager.PERMISSION_GRANTED);
if ((!isInContacts) || (permission == PackageManager.PERMISSION_GRANTED)) {
bindForOutgoingCallerId(theCall);
@@ -1881,7 +2173,7 @@ public class CallsManager extends Call.ListenerBase
setIntentExtrasAndStartTime(callToUse, extras);
setCallSourceToAnalytics(callToUse, originalIntent);
- if (isPotentialMMICode(handle) && !isSelfManaged) {
+ if (mMmiUtils.isPotentialMMICode(handle) && !isSelfManaged) {
// Do not add the call if it is a potential MMI code.
callToUse.addListener(this);
} else if (!mCalls.contains(callToUse)) {
@@ -1895,6 +2187,113 @@ public class CallsManager extends Call.ListenerBase
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;
+ }
+ if (uInfo.isManagedProfile()) {
+ return uInfo.id;
+ }
+ }
+ return UserHandle.USER_NULL;
+ }
+
+ private boolean showSwitchToManagedProfileDialog(Uri callUri, UserHandle initiatingUser,
+ int managedProfileUserId) {
+ // Note that the ACTION_CALL intent will resolve to Telecomm's UserCallActivity
+ // even if there is no dialer. Hence we explicitly check for whether a default dialer
+ // exists instead of relying on ActivityNotFound when sending the call intent.
+ if (TextUtils.isEmpty(
+ mDefaultDialerCache.getDefaultDialerApplication(managedProfileUserId))) {
+ Log.i(
+ this,
+ "Work profile telephony: default dialer app missing, showing error dialog.");
+ return maybeShowErrorDialog(callUri, managedProfileUserId, initiatingUser);
+ }
+
+ UserManager userManager = mContext.getSystemService(UserManager.class);
+ if (userManager.isQuietModeEnabled(UserHandle.of(managedProfileUserId))) {
+ Log.i(
+ this,
+ "Work profile telephony: quiet mode enabled, showing error dialog");
+ return maybeShowErrorDialog(callUri, managedProfileUserId, initiatingUser);
+ }
+ Log.i(
+ this,
+ "Work profile telephony: show forwarding call to managed profile dialog");
+ return maybeRedirectToIntentForwarder(callUri, initiatingUser);
+ }
+
+ private boolean maybeRedirectToIntentForwarder(
+ Uri callUri,
+ UserHandle initiatingUser) {
+ // Note: This intent is selected to match the CALL_MANAGED_PROFILE filter in
+ // DefaultCrossProfileIntentFiltersUtils. This ensures that it is redirected to
+ // IntentForwarderActivity.
+ Intent forwardCallIntent = new Intent(Intent.ACTION_CALL, callUri);
+ forwardCallIntent.addCategory(Intent.CATEGORY_DEFAULT);
+ ResolveInfo resolveInfos =
+ mContext.getPackageManager()
+ .resolveActivityAsUser(
+ forwardCallIntent,
+ ResolveInfoFlags.of(0),
+ initiatingUser.getIdentifier());
+ // Check that the intent will actually open the resolver rather than looping to the personal
+ // profile. This should not happen due to the cross profile intent filters.
+ if (resolveInfos == null
+ || !resolveInfos
+ .getComponentInfo()
+ .getComponentName()
+ .getShortClassName()
+ .equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ Log.w(
+ this,
+ "Work profile telephony: Intent would not resolve to forwarder activity.");
+ return false;
+ }
+
+ try {
+ mContext.startActivityAsUser(forwardCallIntent, initiatingUser);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ Log.e(this, e, "Unable to start call intent for work telephony");
+ return false;
+ }
+ }
+
+ private boolean maybeShowErrorDialog(
+ Uri callUri,
+ int managedProfileUserId,
+ UserHandle initiatingUser) {
+ Intent showErrorIntent =
+ new Intent(
+ TelecomManager.ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG,
+ callUri);
+ showErrorIntent.addCategory(Intent.CATEGORY_DEFAULT);
+ showErrorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ showErrorIntent.putExtra(
+ TelecomManager.EXTRA_MANAGED_PROFILE_USER_ID, managedProfileUserId);
+ if (mContext.getPackageManager()
+ .queryIntentActivitiesAsUser(
+ showErrorIntent,
+ ResolveInfoFlags.of(0),
+ initiatingUser)
+ .isEmpty()) {
+ return false;
+ }
+ try {
+ mContext.startActivityAsUser(showErrorIntent, initiatingUser);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ Log.e(
+ this, e,"Work profile telephony: Unable to show error dialog");
+ return false;
+ }
+ }
+
public void startConference(List<Uri> participants, Bundle clientExtras, String callingPackage,
UserHandle initiatingUser) {
@@ -1934,7 +2333,8 @@ public class CallsManager extends Call.ListenerBase
private void bindForOutgoingCallerId(Call theCall) {
// Find the user chosen call screening app.
String callScreeningApp =
- mRoleManagerAdapter.getDefaultCallScreeningApp();
+ mRoleManagerAdapter.getDefaultCallScreeningApp(
+ theCall.getAssociatedUser());
CompletableFuture future =
new CallScreeningServiceHelper(mContext,
@@ -2090,27 +2490,31 @@ public class CallsManager extends Call.ListenerBase
boolean endEarly = false;
String disconnectReason = "";
- String callRedirectionApp = mRoleManagerAdapter.getDefaultCallRedirectionApp();
+ String callRedirectionApp = mRoleManagerAdapter.getDefaultCallRedirectionApp(
+ phoneAccountHandle.getUserHandle());
PhoneAccount phoneAccount = mPhoneAccountRegistrar
.getPhoneAccountUnchecked(phoneAccountHandle);
if (phoneAccount != null
&& !phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
+ // Note that mCurrentUserHandle may not actually be the current user, i.e.
+ // in the case of work profiles
+ UserHandle currentUserHandle = call.getAssociatedUser();
// Check if the phoneAccountHandle belongs to the current user
if (phoneAccountHandle != null &&
- !phoneAccountHandle.getUserHandle().equals(call.getInitiatingUser())) {
+ !phoneAccountHandle.getUserHandle().equals(currentUserHandle)) {
phoneAccountHandle = null;
}
}
- boolean isPotentialEmergencyNumber;
+ boolean isEmergencyNumber;
try {
- isPotentialEmergencyNumber =
- handle != null && getTelephonyManager().isPotentialEmergencyNumber(
+ isEmergencyNumber =
+ handle != null && getTelephonyManager().isEmergencyNumber(
handle.getSchemeSpecificPart());
} catch (IllegalStateException ise) {
- isPotentialEmergencyNumber = false;
+ isEmergencyNumber = false;
} catch (RuntimeException r) {
- isPotentialEmergencyNumber = false;
+ isEmergencyNumber = false;
}
if (shouldCancelCall) {
@@ -2138,7 +2542,7 @@ public class CallsManager extends Call.ListenerBase
Log.w(this, "onCallRedirectionComplete: phoneAccountHandle is unavailable");
endEarly = true;
disconnectReason = "Unavailable phoneAccountHandle from Call Redirection Service";
- } else if (isPotentialEmergencyNumber) {
+ } else if (isEmergencyNumber) {
Log.w(this, "onCallRedirectionComplete: emergency number %s is redirected from Call"
+ " Redirection Service", handle.getSchemeSpecificPart());
endEarly = true;
@@ -2377,7 +2781,7 @@ public class CallsManager extends Call.ListenerBase
// Auto-enable speakerphone if the originating intent specified to do so, if the call
// is a video call, of if using speaker when docked
PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(
- call.getTargetPhoneAccount(), call.getInitiatingUser());
+ call.getTargetPhoneAccount(), call.getAssociatedUser());
boolean allowVideo = false;
if (account != null) {
allowVideo = account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING);
@@ -2420,12 +2824,26 @@ public class CallsManager extends Call.ListenerBase
// Drop any ongoing self-managed calls to make way for an emergency call.
disconnectSelfManagedCalls("place emerg call" /* reason */);
}
+ try {
+ notifyStartCreateConnection(call);
+ call.startCreateConnection(mPhoneAccountRegistrar);
+ } catch (Exception exception) {
+ // 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.");
+ mAnomalyReporter.reportAnomaly(
+ EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_UUID,
+ EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_MSG);
+ markCallAsDisconnected(call,
+ new DisconnectCause(DisconnectCause.ERROR,
+ "Failed to create the connection."));
+ markCallAsRemoved(call);
+ }
- call.startCreateConnection(mPhoneAccountRegistrar);
}
} else if (mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
requireCallCapableAccountByHandle ? callHandleScheme : null, false,
- call.getInitiatingUser()).isEmpty()) {
+ call.getAssociatedUser(), false).isEmpty()) {
// If there are no call capable accounts, disconnect the call.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.CANCELED,
"No registered PhoneAccounts"));
@@ -2456,6 +2874,10 @@ public class CallsManager extends Call.ListenerBase
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()) {
+ // 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 {
// Hold or disconnect the active call and request call focus for the incoming call.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
@@ -2849,7 +3271,7 @@ public class CallsManager extends Call.ListenerBase
}
@Override
- public void onExtrasChanged(Call c, int source, Bundle extras) {
+ public void onExtrasChanged(Call c, int source, Bundle extras, String requestingPackageName) {
if (source != Call.SOURCE_CONNECTION_SERVICE) {
return;
}
@@ -2877,6 +3299,18 @@ public class CallsManager extends Call.ListenerBase
return constructPossiblePhoneAccounts(handle, user, isVideo, isEmergency, false);
}
+ // Returns whether the device is capable of 2 simultaneous active voice calls on different subs.
+ private boolean isDsdaCallingPossible() {
+ try {
+ return getTelephonyManager().getMaxNumberOfSimultaneouslyActiveSims() > 1
+ || getTelephonyManager().getPhoneCapability()
+ .getMaxActiveVoiceSubscriptions() > 1;
+ } catch (Exception e) {
+ Log.w(this, "exception in isDsdaCallingPossible(): ", e);
+ return false;
+ }
+ }
+
public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
boolean isVideo, boolean isEmergency, boolean isConference) {
@@ -2891,14 +3325,14 @@ public class CallsManager extends Call.ListenerBase
List<PhoneAccountHandle> allAccounts =
mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false, user,
capabilities,
- isEmergency ? 0 : PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY);
- if (mMaxNumberOfSimultaneouslyActiveSims < 0) {
- mMaxNumberOfSimultaneouslyActiveSims =
- getTelephonyManager().getMaxNumberOfSimultaneouslyActiveSims();
- }
+ isEmergency ? 0 : PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY,
+ isEmergency);
// Only one SIM PhoneAccount can be active at one time for DSDS. Only that SIM PhoneAccount
// should be available if a call is already active on the SIM account.
- if (mMaxNumberOfSimultaneouslyActiveSims == 1) {
+ // Similarly, the emergency call should be attempted over the same PhoneAccount as the
+ // ongoing call. However, if the ongoing call is over cross-SIM registration, then the
+ // emergency call will be attempted over a different Phone object at a later stage.
+ if (isEmergency || !isDsdaCallingPossible()) {
List<PhoneAccountHandle> simAccounts =
mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser();
PhoneAccountHandle ongoingCallAccount = null;
@@ -2937,6 +3371,14 @@ public class CallsManager extends Call.ListenerBase
}
}
+ @Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ Log.v(this, "onCallStreamingStateChanged: %b", isStreaming);
+ for (CallsManagerListener listener : mListeners) {
+ listener.onCallStreamingStateChanged(call, isStreaming);
+ }
+ }
+
private void handleCallTechnologyChange(Call call) {
if (call.getExtras() != null
&& call.getExtras().containsKey(TelecomManager.EXTRA_CALL_TECHNOLOGY_TYPE)) {
@@ -2976,6 +3418,14 @@ public class CallsManager extends Call.ListenerBase
mCallAudioManager.setAudioRoute(route, bluetoothAddress);
}
+ /**
+ * Called by the in-call UI to change the CallEndpoint
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+ mCallEndpointController.requestCallEndpointChange(endpoint, callback);
+ }
+
/** Called by the in-call UI to turn the proximity sensor on. */
void turnOnProximitySensor() {
mProximitySensorManager.turnOn();
@@ -3014,7 +3464,7 @@ public class CallsManager extends Call.ListenerBase
} else {
if (setDefault) {
mPhoneAccountRegistrar
- .setUserSelectedOutgoingPhoneAccount(account, call.getInitiatingUser());
+ .setUserSelectedOutgoingPhoneAccount(account, call.getAssociatedUser());
}
if (mPendingAccountSelection != null) {
@@ -3034,6 +3484,30 @@ public class CallsManager extends Call.ListenerBase
}
}
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void updateCallEndpoint(CallEndpoint callEndpoint) {
+ Log.v(this, "updateCallEndpoint");
+ for (CallsManagerListener listener : mListeners) {
+ listener.onCallEndpointChanged(callEndpoint);
+ }
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void updateAvailableCallEndpoints(Set<CallEndpoint> availableCallEndpoints) {
+ Log.v(this, "updateAvailableCallEndpoints");
+ for (CallsManagerListener listener : mListeners) {
+ listener.onAvailableCallEndpointsChanged(availableCallEndpoints);
+ }
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void updateMuteState(boolean isMuted) {
+ Log.v(this, "updateMuteState");
+ for (CallsManagerListener listener : mListeners) {
+ listener.onMuteStateChanged(isMuted);
+ }
+ }
+
/**
* Called when disconnect tone is started or stopped, including any InCallTone
* after disconnected call.
@@ -3053,7 +3527,8 @@ public class CallsManager extends Call.ListenerBase
setCallState(call, CallState.RINGING, "ringing set explicitly");
}
- void markCallAsDialing(Call call) {
+ @VisibleForTesting
+ public void markCallAsDialing(Call call) {
setCallState(call, CallState.DIALING, "dialing set explicitly");
maybeMoveToSpeakerPhone(call);
maybeTurnOffMute(call);
@@ -3070,10 +3545,11 @@ public class CallsManager extends Call.ListenerBase
*/
boolean holdActiveCallForNewCall(Call call) {
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
- Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call, activeCall);
+ Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call.getId(),
+ (activeCall == null ? "<none>" : activeCall.getId()));
if (activeCall != null && activeCall != call) {
if (canHold(activeCall)) {
- activeCall.hold();
+ activeCall.hold("swap to " + call.getId());
return true;
} else if (supportsHold(activeCall)
&& areFromSameSource(activeCall, call)) {
@@ -3099,6 +3575,7 @@ public class CallsManager extends Call.ListenerBase
Log.i(this, "holdActiveCallForNewCall: Holding active %s before making %s active.",
activeCall.getId(), call.getId());
activeCall.hold();
+ call.increaseHeldByThisCallCount();
return true;
} else {
// This call does not support hold. If it is from a different connection
@@ -3124,6 +3601,45 @@ public class CallsManager extends Call.ListenerBase
return false;
}
+ // attempt to hold the requested call and complete the callback on the result
+ public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
+ OutcomeReceiver<Boolean, CallException> callback) {
+ Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "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");
+ 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;
+ }
+
+ // 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);
+ }
+
@VisibleForTesting
public void markCallAsActive(Call call) {
Log.i(this, "markCallAsActive, isSelfManaged: " + call.isSelfManaged());
@@ -3158,7 +3674,6 @@ public class CallsManager extends Call.ListenerBase
}
}
- @VisibleForTesting
public void markCallAsOnHold(Call call) {
setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
}
@@ -3169,8 +3684,9 @@ public class CallsManager extends Call.ListenerBase
*
* @param disconnectCause The disconnect cause, see {@link android.telecom.DisconnectCause}.
*/
- @VisibleForTesting
public void markCallAsDisconnected(Call call, DisconnectCause disconnectCause) {
+ Log.i(this, "markCallAsDisconnected: call=%s; disconnectCause=%s",
+ call.toString(), disconnectCause.toString());
int oldState = call.getState();
if (call.getState() == CallState.SIMULATED_RINGING
&& disconnectCause.getCode() == DisconnectCause.REMOTE) {
@@ -3195,6 +3711,17 @@ public class CallsManager extends Call.ListenerBase
}
}
+ // Notify listeners that the call was disconnected before being added to CallsManager.
+ // Listeners will not receive onAdded or onRemoved callbacks.
+ if (!mCalls.contains(call)) {
+ if (call.isEmergencyCall()) {
+ mAnomalyReporter.reportAnomaly(
+ EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID,
+ EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG);
+ }
+ mListeners.forEach(l -> l.onCreateConnectionFailed(call));
+ }
+
// If a call diagnostic service is in use, we will log the original telephony-provided
// disconnect cause, inform the CDS of the disconnection, and then chain the update of the
// call state until AFTER the CDS reports it's result back.
@@ -3239,7 +3766,7 @@ public class CallsManager extends Call.ListenerBase
/**
* Removes an existing disconnected call, and notifies the in-call app.
*/
- void markCallAsRemoved(Call call) {
+ 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
@@ -3264,38 +3791,57 @@ public class CallsManager extends Call.ListenerBase
* @param call The call.
*/
private void performRemoval(Call call) {
- mInCallController.getBindingFuture().thenRunAsync(() -> {
- call.maybeCleanupHandover();
- removeCall(call);
- Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
- if (mLocallyDisconnectingCalls.contains(call)) {
- boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
- Log.v(this, "performRemoval: isDisconnectingChildCall = "
- + isDisconnectingChildCall + "call -> %s", call);
- mLocallyDisconnectingCalls.remove(call);
- // 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).
- if (!isDisconnectingChildCall && foregroundCall != null
- && foregroundCall.getState() == CallState.ON_HOLD) {
- foregroundCall.unhold();
- }
- } else if (foregroundCall != null &&
- !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
- foregroundCall.getState() == CallState.ON_HOLD) {
-
- // 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)");
+ if (mInCallController.getBindingFuture() != null) {
+ mInCallController.getBindingFuture().thenRunAsync(() -> {
+ doRemoval(call);
+ }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
+ .exceptionally((throwable) -> {
+ Log.e(TAG, throwable, "Error while executing call removal");
+ mAnomalyReporter.reportAnomaly(CALL_REMOVAL_EXECUTION_ERROR_UUID,
+ CALL_REMOVAL_EXECUTION_ERROR_MSG);
+ return null;
+ });
+ } else {
+ doRemoval(call);
+ }
+ }
+
+ /**
+ * Code to perform removal of a call. Called above from {@link #performRemoval(Call)} either
+ * async (in live code) or sync (in testing).
+ * @param call the call to remove.
+ */
+ private void doRemoval(Call call) {
+ call.maybeCleanupHandover();
+ removeCall(call);
+ Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
+ if (mLocallyDisconnectingCalls.contains(call)) {
+ boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
+ Log.v(this, "performRemoval: isDisconnectingChildCall = "
+ + isDisconnectingChildCall + "call -> %s", call);
+ mLocallyDisconnectingCalls.remove(call);
+ // 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).
+ // Also, ensure that the call we're removing is from the same ConnectionService as
+ // the one we're removing. We don't want to auto-unhold between ConnectionService
+ // 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)) {
foregroundCall.unhold();
}
- }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
- .exceptionally((throwable) -> {
- Log.e(TAG, throwable, "Error while executing call removal");
- return null;
- });
+ } else if (foregroundCall != null &&
+ !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
+ foregroundCall.getState() == CallState.ON_HOLD) {
+
+ // 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)");
+ foregroundCall.unhold();
+ }
}
/**
@@ -3365,10 +3911,6 @@ public class CallsManager extends Call.ListenerBase
return false;
}
- boolean hasActiveOrHoldingCall() {
- return getFirstCallWithState(CallState.ACTIVE, CallState.ON_HOLD) != null;
- }
-
boolean hasRingingCall() {
return getFirstCallWithState(CallState.RINGING, CallState.ANSWERED) != null;
}
@@ -3490,15 +4032,6 @@ public class CallsManager extends Call.ListenerBase
return getFirstCallWithState(CallState.ACTIVE);
}
- Call getDialingCall() {
- return getFirstCallWithState(CallState.DIALING);
- }
-
- @VisibleForTesting
- public Call getHeldCall() {
- return getFirstCallWithState(CallState.ON_HOLD);
- }
-
public Call getHeldCallByConnectionService(PhoneAccountHandle targetPhoneAccount) {
Optional<Call> heldCall = mCalls.stream()
.filter(call -> PhoneAccountHandle.areFromSamePackage(call.getTargetPhoneAccount(),
@@ -3622,6 +4155,11 @@ public class CallsManager extends Call.ListenerBase
mClockProxy,
mToastFactory);
+ // 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
+ // telecom where they're added (see below).
+ call.setIsCreateConnectionComplete(true);
+
setCallState(call, Call.getStateFromConnectionState(parcelableConference.getState()),
"new conference call");
call.setHandle(parcelableConference.getHandle(),
@@ -3631,7 +4169,9 @@ public class CallsManager extends Call.ListenerBase
call.setVideoState(parcelableConference.getVideoState());
call.setVideoProvider(parcelableConference.getVideoProvider());
call.setStatusHints(parcelableConference.getStatusHints());
- call.putExtras(Call.SOURCE_CONNECTION_SERVICE, parcelableConference.getExtras());
+ call.putConnectionServiceExtras(parcelableConference.getExtras());
+ // For conference calls, set the associated user from the target phone account user handle.
+ call.setAssociatedUser(phoneAccount.getUserHandle());
// 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();
@@ -3716,10 +4256,15 @@ public class CallsManager extends Call.ListenerBase
*/
@VisibleForTesting
public void addCall(Call call) {
+ if (mCalls.contains(call)) {
+ Log.i(this, "addCall(%s) is already added");
+ return;
+ }
Trace.beginSection("addCall");
Log.i(this, "addCall(%s)", call);
call.addListener(this);
mCalls.add(call);
+ mSelfManagedCallsBeingSetup.remove(call);
// Specifies the time telecom finished routing the call. This is used by the dialer for
// analytics.
@@ -3748,6 +4293,11 @@ public class CallsManager extends Call.ListenerBase
Trace.beginSection("removeCall");
Log.v(this, "removeCall(%s)", call);
+ if (call.isTransactionalCall() && call.getTransactionServiceWrapper() != null) {
+ // remove call from wrappers
+ call.getTransactionServiceWrapper().removeCallFromWrappers(call);
+ }
+
call.setParentAndChildCall(null); // clean up parent relationship before destroying.
call.removeListener(this);
call.clearConnectionService();
@@ -3758,6 +4308,7 @@ public class CallsManager extends Call.ListenerBase
mCalls.remove(call);
shouldNotify = true;
}
+ mSelfManagedCallsBeingSetup.remove(call);
call.destroy();
updateExternalCallCanPullSupport();
@@ -4008,37 +4559,6 @@ public class CallsManager extends Call.ListenerBase
}
}
- private boolean isPotentialMMICode(Uri handle) {
- return (handle != null && handle.getSchemeSpecificPart() != null
- && handle.getSchemeSpecificPart().contains("#"));
- }
-
- /**
- * Determines if a dialed number is potentially an In-Call MMI code. In-Call MMI codes are
- * MMI codes which can be dialed when one or more calls are in progress.
- * <P>
- * Checks for numbers formatted similar to the MMI codes defined in:
- * {@link com.android.internal.telephony.Phone#handleInCallMmiCommands(String)}
- *
- * @param handle The URI to call.
- * @return {@code True} if the URI represents a number which could be an in-call MMI code.
- */
- private boolean isPotentialInCallMMICode(Uri handle) {
- if (handle != null && handle.getSchemeSpecificPart() != null &&
- handle.getScheme() != null &&
- handle.getScheme().equals(PhoneAccount.SCHEME_TEL)) {
-
- String dialedNumber = handle.getSchemeSpecificPart();
- return (dialedNumber.equals("0") ||
- (dialedNumber.startsWith("1") && dialedNumber.length() <= 2) ||
- (dialedNumber.startsWith("2") && dialedNumber.length() <= 2) ||
- dialedNumber.equals("3") ||
- dialedNumber.equals("4") ||
- dialedNumber.equals("5"));
- }
- return false;
- }
-
/**
* Determines if there are any ongoing self managed calls for the given package/user.
* @param packageName The package name to check.
@@ -4046,8 +4566,10 @@ public class CallsManager extends Call.ListenerBase
* @return {@code true} if the app has ongoing calls, or {@code false} otherwise.
*/
public boolean isInSelfManagedCall(String packageName, UserHandle userHandle) {
- return mCalls.stream().anyMatch(
- c -> c.isSelfManaged()
+ return mSelfManagedCallsBeingSetup.stream().anyMatch(c -> c.isSelfManaged()
+ && c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
+ && c.getTargetPhoneAccount().getUserHandle().equals(userHandle)) ||
+ mCalls.stream().anyMatch(c -> c.isSelfManaged()
&& c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
&& c.getTargetPhoneAccount().getUserHandle().equals(userHandle));
}
@@ -4101,6 +4623,60 @@ public class CallsManager extends Call.ListenerBase
return (int) callsStream.count();
}
+ /**
+ * Determines the number of calls (visible to the calling user) matching the specified criteria.
+ * This is an overloaded method which is being used in a security patch to fix up the call
+ * state type APIs which are acting across users when they should not be.
+ *
+ * See {@link TelecomManager#isInCall()} and {@link TelecomManager#isInManagedCall()}.
+ *
+ * @param callFilter indicates whether to include just managed calls
+ * ({@link #CALL_FILTER_MANAGED}), self-managed calls
+ * ({@link #CALL_FILTER_SELF_MANAGED}), or all calls
+ * ({@link #CALL_FILTER_ALL}).
+ * @param excludeCall Where {@code non-null}, this call is excluded from the count.
+ * @param callingUser Where {@code non-null}, call visibility is scoped to this
+ * {@link UserHandle}.
+ * @param hasCrossUserAccess indicates if calling user has the INTERACT_ACROSS_USERS permission.
+ * @param phoneAccountHandle Where {@code non-null}, calls for this {@link PhoneAccountHandle}
+ * are excluded from the count.
+ * @param states The list of {@link CallState}s to include in the count.
+ * @return Count of calls matching criteria.
+ */
+ @VisibleForTesting
+ public int getNumCallsWithState(final int callFilter, Call excludeCall,
+ UserHandle callingUser, boolean hasCrossUserAccess,
+ PhoneAccountHandle phoneAccountHandle, int... states) {
+
+ Set<Integer> desiredStates = IntStream.of(states).boxed().collect(Collectors.toSet());
+
+ Stream<Call> callsStream = mCalls.stream()
+ .filter(call -> desiredStates.contains(call.getState()) &&
+ call.getParentCall() == null && !call.isExternalCall());
+
+ if (callFilter == CALL_FILTER_MANAGED) {
+ callsStream = callsStream.filter(call -> !call.isSelfManaged());
+ } else if (callFilter == CALL_FILTER_SELF_MANAGED) {
+ callsStream = callsStream.filter(call -> call.isSelfManaged());
+ }
+
+ // If a call to exclude was specified, filter it out.
+ if (excludeCall != null) {
+ callsStream = callsStream.filter(call -> call != excludeCall);
+ }
+
+ // If a phone account handle was specified, only consider calls for that phone account.
+ if (phoneAccountHandle != null) {
+ callsStream = callsStream.filter(
+ call -> phoneAccountHandle.equals(call.getTargetPhoneAccount()));
+ }
+
+ callsStream = callsStream.filter(
+ call -> hasCrossUserAccess || isCallVisibleForUser(call, callingUser));
+
+ return (int) callsStream.count();
+ }
+
private boolean hasMaximumLiveCalls(Call exceptCall) {
return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
exceptCall, null /* phoneAccountHandle*/, LIVE_CALL_STATES);
@@ -4185,33 +4761,42 @@ public class CallsManager extends Call.ListenerBase
}
/**
- * Determines if there are any self-managed calls.
+ * Note: isInSelfManagedCall(packageName, UserHandle) should always be used in favor or this
+ * method. This method determines if there are any self-managed calls globally.
* @return {@code true} if there are self-managed calls, {@code false} otherwise.
*/
+ @VisibleForTesting
public boolean hasSelfManagedCalls() {
- return mCalls.stream().filter(call -> call.isSelfManaged()).count() > 0;
+ return mSelfManagedCallsBeingSetup.size() > 0 ||
+ mCalls.stream().filter(call -> call.isSelfManaged()).count() > 0;
}
/**
* Determines if there are any ongoing managed or self-managed calls.
* Note: The {@link #ONGOING_CALL_STATES} are
+ * @param callingUser The user to scope the calls to.
+ * @param hasCrossUserAccess indicates if user has the INTERACT_ACROSS_USERS permission.
* @return {@code true} if there are ongoing managed or self-managed calls, {@code false}
* otherwise.
*/
- public boolean hasOngoingCalls() {
+ public boolean hasOngoingCalls(UserHandle callingUser, boolean hasCrossUserAccess) {
return getNumCallsWithState(
CALL_FILTER_ALL, null /* excludeCall */,
+ callingUser, hasCrossUserAccess,
null /* phoneAccountHandle */,
ONGOING_CALL_STATES) > 0;
}
/**
* Determines if there are any ongoing managed calls.
+ * @param callingUser The user to scope the calls to.
+ * @param hasCrossUserAccess indicates if user has the INTERACT_ACROSS_USERS permission.
* @return {@code true} if there are ongoing managed calls, {@code false} otherwise.
*/
- public boolean hasOngoingManagedCalls() {
+ public boolean hasOngoingManagedCalls(UserHandle callingUser, boolean hasCrossUserAccess) {
return getNumCallsWithState(
CALL_FILTER_MANAGED, null /* excludeCall */,
+ callingUser, hasCrossUserAccess,
null /* phoneAccountHandle */,
ONGOING_CALL_STATES) > 0;
}
@@ -4292,6 +4877,7 @@ public class CallsManager extends Call.ListenerBase
}
// If the user tries to make two outgoing calls to different emergency call numbers,
// we will try to connect the first outgoing call and reject the second.
+ emergencyCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
return false;
}
@@ -4302,6 +4888,12 @@ public class CallsManager extends Call.ListenerBase
return true;
}
+ // If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
+ if (liveCall.getState() == CallState.CONNECTING) {
+ mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID,
+ LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG);
+ }
+
// If we have the max number of held managed calls and we're placing an emergency call,
// we'll disconnect the ongoing call if it cannot be held.
if (hasMaximumManagedHoldingCalls(emergencyCall) && !canHold(liveCall)) {
@@ -4373,12 +4965,14 @@ public class CallsManager extends Call.ListenerBase
if (canHold(liveCall)) {
Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
emergencyCall.getAnalytics().setCallIsAdditional(true);
+ emergencyCall.increaseHeldByThisCallCount();
liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.hold("calling " + emergencyCall.getId());
return true;
}
// The live call cannot be held so we're out of luck here. There's no room.
+ emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
return false;
}
@@ -4400,9 +4994,20 @@ public class CallsManager extends Call.ListenerBase
return true;
}
- // If the live call is stuck in a connecting state, then we should disconnect it in favor
- // of the new outgoing call.
- if (liveCall.getState() == CallState.CONNECTING) {
+ // If the live call is stuck in a connecting state for longer than the transitory timeout,
+ // then we should disconnect it in favor of the new outgoing call and prompt the user to
+ // generate a bugreport.
+ // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+ // live call stuck in the connecting state. Unfortunately that code will get tripped up by
+ // calls that have a longer than expected new outgoing call broadcast response time. This
+ // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+ // block outgoing calls. However, if the user dials two calls in quick succession it will
+ // result in both calls getting disconnected, which is not optimal.
+ if (liveCall.getState() == CallState.CONNECTING
+ && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+ > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+ mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+ LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
liveCall.disconnect("Force disconnect CONNECTING call.");
return true;
}
@@ -4418,6 +5023,7 @@ public class CallsManager extends Call.ListenerBase
+ " of new outgoing call.");
return true;
}
+ call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
return false;
}
@@ -4437,12 +5043,25 @@ public class CallsManager extends Call.ListenerBase
liveCallPhoneAccount);
}
- // First thing, if we are trying to make a call with the same phone account as the live
- // call, then allow it so that the connection service can make its own decision about
- // how to handle the new call relative to the current one.
+ // First thing, for managed calls, if we are trying to make a call with the same phone
+ // account as the live call, then allow it so that the connection service can make its own
+ // decision about how to handle the new call relative to the current one.
+ // Note: This behavior is primarily in place because Telephony historically manages the
+ // state of the calls it tracks by itself, holding and unholding as needed. Self-managed
+ // calls, even though from the same package are normally held/unheld automatically by
+ // Telecom. Calls within a single ConnectionService get held/unheld automatically during
+ // "swap" operations by CallsManager#holdActiveCallForNewCall. There is, however, a quirk
+ // in that if an app declares TWO different ConnectionServices, holdActiveCallForNewCall
+ // would not work correctly because focus switches between ConnectionServices, yet we
+ // tended to assume that if the calls are from the same package that the hold/unhold should
+ // be done by the app. That was a bad assumption as it meant that we could have two active
+ // calls.
+ // TODO(b/280826075): We need to come back and revisit all this logic in a holistic manner.
if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
- call.getTargetPhoneAccount())) {
- Log.i(this, "makeRoomForOutgoingCall: phoneAccount matches.");
+ call.getTargetPhoneAccount())
+ && !call.isSelfManaged()
+ && !liveCall.isSelfManaged()) {
+ Log.i(this, "makeRoomForOutgoingCall: managed phoneAccount matches");
call.getAnalytics().setCallIsAdditional(true);
liveCall.getAnalytics().setCallIsInterrupted(true);
return true;
@@ -4466,6 +5085,7 @@ public class CallsManager extends Call.ListenerBase
}
// The live call cannot be held so we're out of luck here. There's no room.
+ call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
return false;
}
@@ -4509,17 +5129,47 @@ public class CallsManager extends Call.ListenerBase
}
}
+ /**
+ * Ensures that the call will be audible to the user by checking if the voice call stream is
+ * audible, and if not increasing the volume to the default value.
+ */
private void ensureCallAudible() {
- AudioManager am = mContext.getSystemService(AudioManager.class);
- if (am == null) {
- Log.w(this, "ensureCallAudible: audio manager is null");
- return;
- }
- if (am.getStreamVolume(AudioManager.STREAM_VOICE_CALL) == 0) {
- Log.i(this, "ensureCallAudible: voice call stream has volume 0. Adjusting to default.");
- am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
- AudioSystem.getDefaultStreamVolume(AudioManager.STREAM_VOICE_CALL), 0);
- }
+ // Audio manager APIs can be somewhat slow. To prevent a potential ANR we will fire off
+ // this opreation on the async task executor. Note that this operation does not have any
+ // dependency on any Telecom state, so we can safely launch this on a different thread
+ // without worrying that it is in the Telecom sync lock.
+ mAsyncTaskExecutor.execute(() -> {
+ AudioManager am = mContext.getSystemService(AudioManager.class);
+ if (am == null) {
+ Log.w(this, "ensureCallAudible: audio manager is null");
+ return;
+ }
+ if (am.getStreamVolume(AudioManager.STREAM_VOICE_CALL) == 0) {
+ Log.i(this,
+ "ensureCallAudible: voice call stream has volume 0. Adjusting to default.");
+ am.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+ AudioSystem.getDefaultStreamVolume(AudioManager.STREAM_VOICE_CALL), 0);
+ }
+ });
+ }
+
+ /**
+ * Asynchronously updates the emergency call notification.
+ * @param context the context for the update.
+ */
+ private void updateEmergencyCallNotificationAsync(Context context) {
+ mAsyncTaskExecutor.execute(() -> {
+ Log.startSession("CM.UEMCNA");
+ try {
+ boolean shouldShow = mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(
+ context);
+ Log.i(CallsManager.this, "updateEmergencyCallNotificationAsync; show=%b",
+ shouldShow);
+ mBlockedNumbersAdapter.updateEmergencyCallNotification(context, shouldShow);
+ } finally {
+ Log.endSession();
+ }
+ });
}
/**
@@ -4566,8 +5216,11 @@ public class CallsManager extends Call.ListenerBase
call.setHandle(connection.getHandle(), connection.getHandlePresentation());
call.setCallerDisplayName(connection.getCallerDisplayName(),
connection.getCallerDisplayNamePresentation());
+ // For existing connections, use the phone account user handle to determine the user
+ // association with the call.
+ call.setAssociatedUser(connection.getPhoneAccount().getUserHandle());
call.addListener(this);
- call.putExtras(Call.SOURCE_CONNECTION_SERVICE, connection.getExtras());
+ call.putConnectionServiceExtras(connection.getExtras());
Log.i(this, "createCallForExistingConnection: %s", connection);
Call parentCall = null;
@@ -4586,6 +5239,9 @@ public class CallsManager extends Call.ListenerBase
call.setParentCall(parentCall);
}
}
+ // Existing connections originate from a connection service, so they are completed creation
+ // by the ConnectionService implicitly.
+ call.setIsCreateConnectionComplete(true);
addCall(call);
if (parentCall != null) {
// Now, set the call as a child of the parent since it has been added to Telecom. This
@@ -4690,23 +5346,42 @@ public class CallsManager extends Call.ListenerBase
public boolean isIncomingCallPermitted(Call excludeCall,
PhoneAccountHandle phoneAccountHandle) {
+ return checkIncomingCallPermitted(excludeCall, phoneAccountHandle).isSuccess();
+ }
+
+ private CallFailureCause checkIncomingCallPermitted(
+ Call call, PhoneAccountHandle phoneAccountHandle) {
if (phoneAccountHandle == null) {
- return false;
+ return CallFailureCause.INVALID_USE;
}
+
PhoneAccount phoneAccount =
mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
if (phoneAccount == null) {
- return false;
+ return CallFailureCause.INVALID_USE;
}
- if (isInEmergencyCall()) return false;
- if (!phoneAccount.isSelfManaged()) {
- return !hasMaximumManagedRingingCalls(excludeCall) &&
- !hasMaximumManagedHoldingCalls(excludeCall);
+ if (isInEmergencyCall()) {
+ return CallFailureCause.IN_EMERGENCY_CALL;
+ }
+
+ if (phoneAccount.isSelfManaged()) {
+ if (hasMaximumSelfManagedRingingCalls(call, phoneAccountHandle)) {
+ return CallFailureCause.MAX_RINGING_CALLS;
+ }
+ if (hasMaximumSelfManagedCalls(call, phoneAccountHandle)) {
+ return CallFailureCause.MAX_SELF_MANAGED_CALLS;
+ }
} else {
- return !hasMaximumSelfManagedRingingCalls(excludeCall, phoneAccountHandle) &&
- !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle);
+ if (hasMaximumManagedRingingCalls(call)) {
+ return CallFailureCause.MAX_RINGING_CALLS;
+ }
+ if (hasMaximumManagedHoldingCalls(call)) {
+ return CallFailureCause.MAX_HOLD_CALLS;
+ }
}
+
+ return CallFailureCause.NONE;
}
public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
@@ -4878,7 +5553,7 @@ public class CallsManager extends Call.ListenerBase
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
*/
- public void dump(IndentingPrintWriter pw) {
+ public void dump(IndentingPrintWriter pw, String[] args) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
if (mCalls != null) {
pw.println("mCalls: ");
@@ -4934,6 +5609,20 @@ public class CallsManager extends Call.ListenerBase
pw.decreaseIndent();
}
+ if (mCallAnomalyWatchdog != null) {
+ pw.println("mCallAnomalyWatchdog:");
+ pw.increaseIndent();
+ mCallAnomalyWatchdog.dump(pw);
+ pw.decreaseIndent();
+ }
+
+ if (mEmergencyCallDiagnosticLogger != null) {
+ pw.println("mEmergencyCallDiagnosticLogger:");
+ pw.increaseIndent();
+ mEmergencyCallDiagnosticLogger.dump(pw, args);
+ pw.decreaseIndent();
+ }
+
if (mDefaultDialerCache != null) {
pw.println("mDefaultDialerCache:");
pw.increaseIndent();
@@ -4955,6 +5644,13 @@ public class CallsManager extends Call.ListenerBase
impl.dump(pw);
pw.decreaseIndent();
}
+
+ if (mConnectionSvrFocusMgr != null) {
+ pw.println("mConnectionSvrFocusMgr:");
+ pw.increaseIndent();
+ mConnectionSvrFocusMgr.dump(pw);
+ pw.decreaseIndent();
+ }
}
/**
@@ -4963,11 +5659,14 @@ public class CallsManager extends Call.ListenerBase
* @param call The call.
*/
private void maybeShowErrorDialogOnDisconnect(Call call) {
- if (call.getState() == CallState.DISCONNECTED && (isPotentialMMICode(call.getHandle())
- || isPotentialInCallMMICode(call.getHandle())) && !mCalls.contains(call)) {
+ if (call.getState() == CallState.DISCONNECTED && (mMmiUtils.isPotentialMMICode(
+ call.getHandle())
+ || mMmiUtils.isPotentialInCallMMICode(call.getHandle())) && !mCalls.contains(
+ call)) {
DisconnectCause disconnectCause = call.getDisconnectCause();
- if (!TextUtils.isEmpty(disconnectCause.getDescription()) && (disconnectCause.getCode()
- == DisconnectCause.ERROR)) {
+ if (!TextUtils.isEmpty(disconnectCause.getDescription()) && ((disconnectCause.getCode()
+ == DisconnectCause.ERROR) || (disconnectCause.getCode()
+ == DisconnectCause.RESTRICTED))) {
Intent errorIntent = new Intent(mContext, ErrorDialogActivity.class);
errorIntent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_STRING_EXTRA,
disconnectCause.getDescription());
@@ -5039,6 +5738,9 @@ public class CallsManager extends Call.ListenerBase
} else {
call.setConnectionService(service);
service.createConnectionFailed(call);
+ if (!mCalls.contains(call)){
+ mListeners.forEach(l -> l.onCreateConnectionFailed(call));
+ }
}
}
@@ -5061,9 +5763,20 @@ public class CallsManager extends Call.ListenerBase
} else {
call.setConnectionService(service);
service.createConferenceFailed(call);
+ if (!mCalls.contains(call)){
+ mListeners.forEach(l -> l.onCreateConnectionFailed(call));
+ }
}
}
+ /**
+ * Notify interested parties that a new call is about to be handed off to a ConnectionService to
+ * be created.
+ * @param theCall the new call.
+ */
+ private void notifyStartCreateConnection(final Call theCall) {
+ mListeners.forEach(l -> l.onStartCreateConnection(theCall));
+ }
/**
* Notifies the {@link android.telecom.ConnectionService} associated with a
@@ -5155,7 +5868,9 @@ public class CallsManager extends Call.ListenerBase
if (isSelfManaged) {
call.setIsVoipAudioMode(true);
}
- call.setInitiatingUser(getCurrentUserHandle());
+ // Set associated user based on the existing call as it doesn't make sense to handover calls
+ // across user profiles.
+ call.setAssociatedUser(handoverFromCall.getAssociatedUser());
// Ensure we don't try to place an outgoing call with video if video is not
// supported.
@@ -5227,7 +5942,7 @@ public class CallsManager extends Call.ListenerBase
// Disconnect all self-managed calls to make priority for emergency call.
disconnectSelfManagedCalls("emergency call");
}
-
+ notifyStartCreateConnection(call);
call.startCreateConnection(mPhoneAccountRegistrar);
}
@@ -5389,6 +6104,9 @@ public class CallsManager extends Call.ListenerBase
fromCall.setHandoverDestinationCall(call);
call.setHandoverSourceCall(fromCall);
call.setHandoverState(HandoverState.HANDOVER_TO_STARTED);
+ // Set associated user based on the existing call as it doesn't make sense to handover calls
+ // across user profiles.
+ call.setAssociatedUser(fromCall.getAssociatedUser());
fromCall.setHandoverState(HandoverState.HANDOVER_FROM_STARTED);
if (isSpeakerEnabledForVideoCalls() && VideoProfile.isVideo(videoState)) {
@@ -5404,7 +6122,7 @@ public class CallsManager extends Call.ListenerBase
extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER_CONNECTION, true);
extras.putParcelable(TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT,
fromCall.getTargetPhoneAccount());
-
+ notifyStartCreateConnection(call);
call.startCreateConnection(mPhoneAccountRegistrar);
}
@@ -5412,8 +6130,10 @@ public class CallsManager extends Call.ListenerBase
return mConnectionSvrFocusMgr;
}
- private boolean canHold(Call call) {
- return call.can(Connection.CAPABILITY_HOLD) && call.getState() != CallState.DIALING;
+ @VisibleForTesting
+ public boolean canHold(Call call) {
+ return ((call.isTransactionalCall() && call.can(Connection.CAPABILITY_SUPPORT_HOLD)) ||
+ call.can(Connection.CAPABILITY_HOLD)) && call.getState() != CallState.DIALING;
}
private boolean supportsHold(Call call) {
@@ -5435,8 +6155,12 @@ public class CallsManager extends Call.ListenerBase
@Override
public void performAction() {
synchronized (mLock) {
- Log.d(this, "perform set call state for %s, state = %s", mCall, mState);
- setCallState(mCall, mState, mTag);
+ Log.d(this, "performAction: current call state %s", mCall);
+ if (mCall.getState() != CallState.DISCONNECTED
+ && mCall.getState() != CallState.DISCONNECTING) {
+ Log.d(this, "performAction: setting to new state = %s", mState);
+ setCallState(mCall, mState, mTag);
+ }
}
}
}
@@ -5517,6 +6241,82 @@ public class CallsManager extends Call.ListenerBase
}
}
+ /**
+ * This helper mainly requests mConnectionSvrFocusMgr to update the call focus via a
+ * {@link TransactionalFocusRequestCallback}. However, in the case of a held call, the
+ * state must be set first and then a request must be made.
+ *
+ * @param newCallFocus to set active/answered
+ * @param resultCallback that back propagates the focusManager result
+ *
+ * Note: This method should only be called if there are no active calls.
+ */
+ public void requestNewCallFocusAndVerify(Call newCallFocus,
+ OutcomeReceiver<Boolean, CallException> resultCallback) {
+ int currentCallState = newCallFocus.getState();
+ PendingAction pendingAction = null;
+
+ // if the current call is in a state that can become the new call focus, we can set the
+ // state afterwards...
+ if (ConnectionServiceFocusManager.PRIORITY_FOCUS_CALL_STATE.contains(currentCallState)) {
+ pendingAction = new ActionSetCallState(newCallFocus, CallState.ACTIVE,
+ "vCFC: pending action set state");
+ } else {
+ // However, HELD calls need to be set to ACTIVE before requesting call focus.
+ setCallState(newCallFocus, CallState.ACTIVE, "vCFC: immediately set active");
+ }
+
+ mConnectionSvrFocusMgr
+ .requestFocus(newCallFocus,
+ new TransactionalFocusRequestCallback(pendingAction, currentCallState,
+ newCallFocus, resultCallback));
+ }
+
+ /**
+ * Request a new call focus and ensure the request was successful via an OutcomeReceiver. Also,
+ * conditionally include a PendingAction that will execute if and only if the call focus change
+ * is successful.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public class TransactionalFocusRequestCallback implements
+ ConnectionServiceFocusManager.RequestFocusCallback {
+ private PendingAction mPendingAction;
+ private int mPreviousCallState;
+ @NonNull private Call mTargetCallFocus;
+ private OutcomeReceiver<Boolean, CallException> mCallback;
+
+ TransactionalFocusRequestCallback(PendingAction pendingAction, int previousState,
+ @NonNull Call call, OutcomeReceiver<Boolean, CallException> callback) {
+ mPendingAction = pendingAction;
+ mPreviousCallState = previousState;
+ mTargetCallFocus = call;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onRequestFocusDone(ConnectionServiceFocusManager.CallFocus call) {
+ Call currentCallFocus = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+ // verify the update was successful before updating the state
+ Log.i(this, "tFRC: currentCallFocus=[%s], targetFocus=[%s]",
+ mTargetCallFocus, currentCallFocus);
+ if (currentCallFocus == null ||
+ !currentCallFocus.getId().equals(mTargetCallFocus.getId())) {
+ // possibly reset the call state
+ if (mTargetCallFocus.getState() != mPreviousCallState) {
+ mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
+ }
+ mCallback.onError(new CallException("failed to switch focus to requested call",
+ CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+ return;
+ }
+ // at this point, we know the FocusManager is able to update successfully
+ if (mPendingAction != null) {
+ mPendingAction.performAction(); // set the call state
+ }
+ mCallback.onResult(true); // complete the transaction
+ }
+ }
+
public void resetConnectionTime(Call call) {
call.setConnectTimeMillis(System.currentTimeMillis());
call.setConnectElapsedTimeMillis(SystemClock.elapsedRealtime());
@@ -5536,7 +6336,8 @@ public class CallsManager extends Call.ListenerBase
* call, or a number which has been identified by the number as an emergency call.
* @return {@code true} if there is an ongoing emergency call, {@code false} otherwise.
*/
- public boolean isInEmergencyCall() {
+ public boolean
+ isInEmergencyCall() {
return mCalls.stream().filter(c -> (c.isEmergencyCall()
|| c.isNetworkIdentifiedEmergencyCall()) && !c.isDisconnected()).count() > 0;
}
@@ -5585,6 +6386,19 @@ public class CallsManager extends Call.ListenerBase
}
/**
+ * Determines if a {@link Call} is visible to the calling user. If the {@link PhoneAccount} has
+ * CAPABILITY_MULTI_USER, or the user handle associated with the {@link PhoneAccount} is the
+ * same as the calling user, the call is visible to the user.
+ * @param call
+ * @return {@code true} if call is visible to the calling user
+ */
+ boolean isCallVisibleForUser(Call call, UserHandle userHandle) {
+ return call.getAssociatedUser().equals(userHandle)
+ || call.getPhoneAccountFromHandle()
+ .hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER);
+ }
+
+ /**
* Determines if two {@link Call} instances originated from either the same target
* {@link PhoneAccountHandle} or connection manager {@link PhoneAccountHandle}.
* @param call1 The first call
@@ -5665,4 +6479,58 @@ public class CallsManager extends Call.ListenerBase
public Ringer getRinger() {
return mRinger;
}
+
+ @VisibleForTesting
+ public VoipCallMonitor getVoipCallMonitor() {
+ return mVoipCallMonitor;
+ }
+
+ /**
+ * This method should only be used for testing.
+ */
+ @VisibleForTesting
+ public void createActionSetCallStateAndPerformAction(Call call, int state, String tag) {
+ ActionSetCallState actionSetCallState = new ActionSetCallState(call, state, tag);
+ actionSetCallState.performAction();
+ }
+
+ public CallStreamingController getCallStreamingController() {
+ return mCallStreamingController;
+ }
+
+ /**
+ * Given a call identified by call id, get the instance from the list of calls.
+ * @param callId the call id.
+ * @return the call, or null if not found.
+ */
+ public @Nullable Call getCall(@NonNull String callId) {
+ Optional<Call> foundCall = mCalls.stream().filter(
+ c -> c.getId().equals(callId)).findFirst();
+ if (foundCall.isPresent()) {
+ return foundCall.get();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Triggers stopping of call streaming for a call by launching a stop streaming transaction.
+ * @param call the call.
+ */
+ public void stopCallStreaming(@NonNull Call call) {
+ if (call.getTransactionServiceWrapper() == null) {
+ return;
+ }
+ call.getTransactionServiceWrapper().stopCallStreaming(call);
+ }
+
+ @VisibleForTesting
+ public Set<Call> getSelfManagedCallsBeingSetup() {
+ return mSelfManagedCallsBeingSetup;
+ }
+
+ @VisibleForTesting
+ public void addCallBeingSetup(Call call) {
+ mSelfManagedCallsBeingSetup.add(call);
+ }
}
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 55c7b53d7..43f3b906c 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -18,12 +18,14 @@ 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;
/**
* Provides a default implementation for listeners of CallsManager.
*/
-public class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
+public abstract class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
@Override
public void onCallAdded(Call call) {
}
@@ -33,6 +35,10 @@ public class CallsManagerListenerBase implements CallsManager.CallsManagerListen
}
@Override
+ public void onCreateConnectionFailed(Call call) {
+ }
+
+ @Override
public void onCallStateChanged(Call call, int oldState, int newState) {
}
@@ -57,6 +63,18 @@ public class CallsManagerListenerBase implements CallsManager.CallsManagerListen
}
@Override
+ public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+ }
+
+ @Override
+ public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
+ }
+
+ @Override
+ public void onMuteStateChanged(boolean isMuted) {
+ }
+
+ @Override
public void onRingbackRequested(Call call, boolean ringback) {
}
@@ -90,6 +108,10 @@ public class CallsManagerListenerBase implements CallsManager.CallsManagerListen
}
@Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ }
+
+ @Override
public void onDisconnectedTonePlaying(boolean isTonePlaying) {
}
diff --git a/src/com/android/server/telecom/CarModeTracker.java b/src/com/android/server/telecom/CarModeTracker.java
index 737ce5a82..ae8febfe8 100644
--- a/src/com/android/server/telecom/CarModeTracker.java
+++ b/src/com/android/server/telecom/CarModeTracker.java
@@ -150,7 +150,9 @@ public class CarModeTracker {
Log.i(this, "handleExitCarMode: packageName=%s, priority=%d", packageName, priority);
mCarModeChangeLog.log("exitCarMode: packageName=" + packageName + ", priority="
+ priority);
- mCarModeApps.removeIf(c -> c.getPriority() == priority);
+
+ //Remove the car mode app with specified priority without clearing out the projection entry.
+ mCarModeApps.removeIf(c -> c.getPriority() == priority && !c.hasSetAutomotiveProjection());
}
public void handleSetAutomotiveProjection(@NonNull String packageName) {
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index aa0a64f69..3694727a2 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -25,13 +25,16 @@ import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.text.TextUtils;
+import android.util.LocalLog;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -40,6 +43,7 @@ import java.util.stream.Collectors;
public class ConnectionServiceFocusManager {
private static final String TAG = "ConnectionSvrFocusMgr";
private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000;
+ private final LocalLog mLocalLog = new LocalLog(20);
/** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */
public interface ConnectionServiceFocusManagerFactory {
@@ -123,6 +127,11 @@ public class ConnectionServiceFocusManager {
* @return {@code True} if this call can receive focus, {@code false} otherwise.
*/
boolean isFocusable();
+
+ /**
+ * @return the ID of the focusable for debug purposes.
+ */
+ String getId();
}
/** Interface define a call back for focus request event. */
@@ -153,9 +162,9 @@ public class ConnectionServiceFocusManager {
void setCallsManagerListener(CallsManager.CallsManagerListener listener);
}
- private static final int[] PRIORITY_FOCUS_CALL_STATE = new int[] {
- CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING, CallState.AUDIO_PROCESSING
- };
+ public static final Set<Integer> PRIORITY_FOCUS_CALL_STATE
+ = Set.of(CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING,
+ CallState.AUDIO_PROCESSING, CallState.RINGING);
private static final int MSG_REQUEST_FOCUS = 1;
private static final int MSG_RELEASE_CONNECTION_FOCUS = 2;
@@ -348,20 +357,23 @@ public class ConnectionServiceFocusManager {
public List<CallFocus> getAllCall() { return mCalls; }
private void updateConnectionServiceFocus(ConnectionServiceFocus connSvrFocus) {
+ Log.i(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus);
if (!Objects.equals(mCurrentFocus, connSvrFocus)) {
if (connSvrFocus != null) {
connSvrFocus.setConnectionServiceFocusListener(mConnectionServiceFocusListener);
connSvrFocus.connectionServiceFocusGained();
}
mCurrentFocus = connSvrFocus;
- Log.d(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus);
+ Log.i(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus);
}
}
private void updateCurrentFocusCall() {
+ CallFocus previousFocus = mCurrentFocusCall;
mCurrentFocusCall = null;
if (mCurrentFocus == null) {
+ Log.i(this, "updateCurrentFocusCall: mCurrentFocus is null");
return;
}
@@ -371,17 +383,20 @@ public class ConnectionServiceFocusManager {
&& call.isFocusable())
.collect(Collectors.toList());
- for (int i = 0; i < PRIORITY_FOCUS_CALL_STATE.length; i++) {
- for (CallFocus call : calls) {
- if (call.getState() == PRIORITY_FOCUS_CALL_STATE[i]) {
- mCurrentFocusCall = call;
- Log.d(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
- return;
+ for (CallFocus call : calls) {
+ if (PRIORITY_FOCUS_CALL_STATE.contains(call.getState())) {
+ mCurrentFocusCall = call;
+ if (previousFocus != call) {
+ mLocalLog.log(call.getId());
}
+ Log.i(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
+ return;
}
}
-
- Log.d(this, "updateCurrentFocusCall = null");
+ if (previousFocus != null) {
+ mLocalLog.log("<none>");
+ }
+ Log.i(this, "updateCurrentFocusCall = null");
}
private void onRequestFocusDone(FocusRequest focusRequest) {
@@ -476,6 +491,11 @@ public class ConnectionServiceFocusManager {
}
}
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("Call Focus History:");
+ mLocalLog.dump(pw);
+ }
+
private final class FocusManagerHandler extends Handler {
FocusManagerHandler(Looper looper) {
super(looper);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 7237c701c..728e121f5 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -23,15 +23,22 @@ import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
+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;
import android.telecom.CallAudioState;
-import android.telecom.CallScreeningService;
+import android.telecom.CallEndpoint;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
@@ -42,11 +49,15 @@ import android.telecom.Logging.Session;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccountHandle;
+import android.telecom.QueryLocationException;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.CellIdentity;
import android.telephony.TelephonyManager;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IConnectionService;
@@ -61,7 +72,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.Objects;
/**
@@ -75,6 +90,9 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
ConnectionServiceFocusManager.ConnectionServiceFocus {
private static final String TELECOM_ABBREVIATION = "cast";
+ private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
+ private @Nullable CancellationSignal mOngoingQueryLocationRequest = null;
+ private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor();
private final class Adapter extends IConnectionServiceAdapter.Stub {
@@ -83,10 +101,17 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
ParcelableConnection connection, Session.Info sessionInfo) {
Log.startSession(sessionInfo, LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
mPackageAbbreviation);
+ UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
+ // Check status hints image for cross user access
+ if (connection.getStatusHints() != null) {
+ Icon icon = connection.getStatusHints().getIcon();
+ connection.getStatusHints().setIcon(StatusHints.
+ validateAccountIconUserBoundary(icon, callingUserHandle));
+ }
ConnectionServiceWrapper.this
.handleCreateConnectionComplete(callId, request, connection);
@@ -358,9 +383,8 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
mCallsManager.markCallAsDisconnected(
call, new DisconnectCause(DisconnectCause.REMOTE));
- } else {
- mCallsManager.markCallAsRemoved(call);
}
+ mCallsManager.markCallAsRemoved(call);
}
}
} catch (Throwable t) {
@@ -435,7 +459,13 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
childCall.setParentAndChildCall(null);
} else {
Call conferenceCall = mCallIdMapper.getCall(conferenceCallId);
- childCall.setParentAndChildCall(conferenceCall);
+ // In a situation where a cmgr is used, the conference should be tracked
+ // by that cmgr's instance of CSW. The cmgr instance of CSW will track
+ // and properly set the parent and child calls so the request from the
+ // original Telephony instance of CSW can be ignored.
+ if (conferenceCall != null){
+ childCall.setParentAndChildCall(conferenceCall);
+ }
}
} else {
// Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
@@ -482,6 +512,14 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
Log.startSession(sessionInfo, LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
mPackageAbbreviation);
+ UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ // Check status hints image for cross user access
+ if (parcelableConference.getStatusHints() != null) {
+ Icon icon = parcelableConference.getStatusHints().getIcon();
+ parcelableConference.getStatusHints().setIcon(StatusHints.
+ validateAccountIconUserBoundary(icon, callingUserHandle));
+ }
+
if (parcelableConference.getConnectElapsedTimeMillis() != 0
&& mContext.checkCallingOrSelfPermission(MODIFY_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
@@ -724,13 +762,40 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
}
@Override
+ public void requestCallEndpointChange(String callId, CallEndpoint endpoint,
+ ResultReceiver callback, Session.Info sessionInfo) {
+ Log.startSession(sessionInfo, "CSW.rCEC", mPackageAbbreviation);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ logIncoming("requestCallEndpointChange %s %s", callId,
+ endpoint.getEndpointName());
+ mCallsManager.requestCallEndpointChange(endpoint, callback);
+ }
+ } catch (Throwable t) {
+ Log.e(ConnectionServiceWrapper.this, t, "");
+ throw t;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
+ }
+ }
+
+ @Override
public void setStatusHints(String callId, StatusHints statusHints,
Session.Info sessionInfo) {
Log.startSession(sessionInfo, "CSW.sSH", mPackageAbbreviation);
+ UserHandle callingUserHandle = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setStatusHints %s %s", callId, statusHints);
+ // Check status hints image for cross user access
+ if (statusHints != null) {
+ Icon icon = statusHints.getIcon();
+ statusHints.setIcon(StatusHints.validateAccountIconUserBoundary(
+ icon, callingUserHandle));
+ }
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
call.setStatusHints(statusHints);
@@ -754,7 +819,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
Bundle.setDefusable(extras, true);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
- call.putExtras(Call.SOURCE_CONNECTION_SERVICE, extras);
+ call.putConnectionServiceExtras(extras);
}
}
} catch (Throwable t) {
@@ -877,6 +942,9 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
callingPhoneAccountHandle.getComponentName().getPackageName());
}
+ boolean hasCrossUserAccess = mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS)
+ == PackageManager.PERMISSION_GRANTED;
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -888,7 +956,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
// an emergency call.
mPhoneAccountRegistrar.getCallCapablePhoneAccounts(null /*uriScheme*/,
false /*includeDisabledAccounts*/, userHandle, 0 /*capabilities*/,
- 0 /*excludedCapabilities*/);
+ 0 /*excludedCapabilities*/, hasCrossUserAccess);
PhoneAccountHandle phoneAccountHandle = null;
for (PhoneAccountHandle accountHandle : accountHandles) {
if(accountHandle.equals(callingPhoneAccountHandle)) {
@@ -962,6 +1030,14 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
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);
@@ -1196,6 +1272,71 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
Log.endSession();
}
}
+
+ @Override
+ public void queryLocation(String callId, long timeoutMillis, String provider,
+ ResultReceiver callback, Session.Info sessionInfo) {
+ Log.startSession(sessionInfo, "CSW.qL", mPackageAbbreviation);
+
+ TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+ if (telecomManager == null || !telecomManager.getSimCallManager().getComponentName()
+ .equals(getComponentName())) {
+ callback.send(0 /* isSuccess */,
+ getQueryLocationErrorResult(QueryLocationException.ERROR_NOT_PERMITTED));
+ Log.endSession();
+ return;
+ }
+
+ String opPackageName = mContext.getOpPackageName();
+ int packageUid = -1;
+ try {
+ packageUid = mContext.getPackageManager().getPackageUid(opPackageName,
+ PackageManager.PackageInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ // packageUid is -1
+ }
+
+ try {
+ mAppOpsManager.noteProxyOp(
+ AppOpsManager.OPSTR_FINE_LOCATION,
+ opPackageName,
+ packageUid,
+ null,
+ null);
+ } catch (SecurityException e) {
+ Log.e(ConnectionServiceWrapper.this, e, "");
+ }
+
+ if (!callingUidMatchesPackageManagerRecords(getComponentName().getPackageName())) {
+ throw new SecurityException(String.format("queryCurrentLocation: "
+ + "uid mismatch found : callingPackageName=[%s], callingUid=[%d]",
+ getComponentName().getPackageName(), Binder.getCallingUid()));
+ }
+
+ Call call = mCallIdMapper.getCall(callId);
+ if (call == null || !call.isEmergencyCall()) {
+ callback.send(0 /* isSuccess */,
+ getQueryLocationErrorResult(QueryLocationException
+ .ERROR_NOT_ALLOWED_FOR_NON_EMERGENCY_CONNECTIONS));
+ Log.endSession();
+ return;
+ }
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ logIncoming("queryLocation %s %d", callId, timeoutMillis);
+ ConnectionServiceWrapper.this.queryCurrentLocation(timeoutMillis, provider,
+ callback);
+ }
+ } catch (Throwable t) {
+ Log.e(ConnectionServiceWrapper.this, t, "");
+ throw t;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
+ }
+ }
}
private final Adapter mAdapter = new Adapter();
@@ -1222,7 +1363,8 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
* @param context The context.
* @param userHandle The {@link UserHandle} to use when binding.
*/
- ConnectionServiceWrapper(
+ @VisibleForTesting
+ public ConnectionServiceWrapper(
ComponentName componentName,
ConnectionServiceRepository connectionServiceRepository,
PhoneAccountRegistrar phoneAccountRegistrar,
@@ -1282,6 +1424,141 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
return null;
}
+ @VisibleForTesting
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void queryCurrentLocation(long timeoutMillis, String provider, ResultReceiver callback) {
+
+ if (mQueryLocationFuture != null && !mQueryLocationFuture.isDone()) {
+ callback.send(0 /* isSuccess */,
+ getQueryLocationErrorResult(
+ QueryLocationException.ERROR_PREVIOUS_REQUEST_EXISTS));
+ return;
+ }
+
+ LocationManager locationManager = (LocationManager) mContext.createAttributionContext(
+ ConnectionServiceWrapper.class.getSimpleName()).getSystemService(
+ Context.LOCATION_SERVICE);
+
+ if (locationManager == null) {
+ callback.send(0 /* isSuccess */,
+ getQueryLocationErrorResult(QueryLocationException.ERROR_SERVICE_UNAVAILABLE));
+ }
+
+ mQueryLocationFuture = new CompletableFuture<Pair<Integer, Location>>()
+ .completeOnTimeout(
+ Pair.create(QueryLocationException.ERROR_REQUEST_TIME_OUT, null),
+ timeoutMillis, TimeUnit.MILLISECONDS);
+
+ mOngoingQueryLocationRequest = new CancellationSignal();
+ locationManager.getCurrentLocation(
+ provider,
+ new LocationRequest.Builder(0)
+ .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
+ .setLocationSettingsIgnored(true)
+ .build(),
+ mOngoingQueryLocationRequest,
+ mQueryLocationExecutor,
+ (location) -> mQueryLocationFuture.complete(Pair.create(null, location)));
+
+ mQueryLocationFuture.whenComplete((result, e) -> {
+ if (e != null) {
+ callback.send(0,
+ getQueryLocationErrorResult(QueryLocationException.ERROR_UNSPECIFIED));
+ }
+ //make sure we don't pass mock locations diretly, always reset() mock locations
+ if (result.second != null) {
+ if(result.second.isMock()) {
+ result.second.reset();
+ }
+ callback.send(1, getQueryLocationResult(result.second));
+ } else {
+ callback.send(0, getQueryLocationErrorResult(result.first));
+ }
+
+ if (mOngoingQueryLocationRequest != null) {
+ mOngoingQueryLocationRequest.cancel();
+ mOngoingQueryLocationRequest = null;
+ }
+
+ if (mQueryLocationFuture != null) {
+ mQueryLocationFuture = null;
+ }
+ });
+ }
+
+ private Bundle getQueryLocationResult(Location location) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(Connection.EXTRA_KEY_QUERY_LOCATION, location);
+ return extras;
+ }
+
+ private Bundle getQueryLocationErrorResult(int result) {
+ String message;
+
+ switch (result) {
+ case QueryLocationException.ERROR_REQUEST_TIME_OUT:
+ message = "The operation was not completed on time";
+ break;
+ case QueryLocationException.ERROR_PREVIOUS_REQUEST_EXISTS:
+ message = "The operation was rejected due to a previous request exists";
+ break;
+ case QueryLocationException.ERROR_NOT_PERMITTED:
+ message = "The operation is not permitted";
+ break;
+ case QueryLocationException.ERROR_NOT_ALLOWED_FOR_NON_EMERGENCY_CONNECTIONS:
+ message = "Non-emergency call connection are not allowed";
+ break;
+ case QueryLocationException.ERROR_SERVICE_UNAVAILABLE:
+ message = "The operation has failed due to service is not available";
+ break;
+ default:
+ message = "The operation has failed due to an unknown or unspecified error";
+ }
+
+ QueryLocationException exception = new QueryLocationException(message, result);
+ Bundle extras = new Bundle();
+ extras.putParcelable(QueryLocationException.QUERY_LOCATION_ERROR, exception);
+ return extras;
+ }
+
+ /**
+ * helper method that compares the binder_uid to what the packageManager_uid reports for the
+ * passed in packageName.
+ *
+ * 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{
+ pm = mContext.createContextAsUser(
+ UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
+ }
+ catch (Exception e){
+ Log.i(this, "callingUidMatchesPackageManagerRecords:"
+ + " createContextAsUser hit exception=[%s]", e.toString());
+ return false;
+ }
+
+ if (pm != null) {
+ try {
+ packageUid = pm.getPackageUid(packageName, PackageManager.PackageInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ // packageUid is -1.
+ }
+ }
+
+ if (packageUid != callingUid) {
+ Log.i(this, "callingUidMatchesPackageManagerRecords: uid mismatch found for "
+ + "packageName=[%s]. packageManager reports packageUid=[%d] but "
+ + "binder reports callingUid=[%d]", packageName, packageUid, callingUid);
+ }
+
+ return packageUid == callingUid;
+ }
+
/**
* Creates a conference for a new outgoing call or attach to an existing incoming call.
*/
@@ -1357,7 +1634,7 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
public void onSuccess() {
String callId = mCallIdMapper.getCallId(call);
if (callId == null) {
- Log.w(ConnectionServiceWrapper.this, "Call not present"
+ Log.i(ConnectionServiceWrapper.this, "Call not present"
+ " in call id mapper, maybe it was aborted before the bind"
+ " completed successfully?");
response.handleCreateConnectionFailure(
@@ -1674,6 +1951,54 @@ public class ConnectionServiceWrapper extends ServiceBinder implements
}
}
+ /** @see IConnectionService#onCallEndpointChanged(String, CallEndpoint, Session.Info) */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint) {
+ final String callId = mCallIdMapper.getCallId(activeCall);
+ if (callId != null && isServiceValid("onCallEndpointChanged")) {
+ try {
+ logOutgoing("onCallEndpointChanged %s %s", callId, callEndpoint);
+ mServiceInterface.onCallEndpointChanged(callId, callEndpoint,
+ Log.getExternalSession(TELECOM_ABBREVIATION));
+ } catch (RemoteException e) {
+ Log.d(this, "Remote exception calling onCallEndpointChanged");
+ }
+ }
+ }
+
+ /** @see IConnectionService#onAvailableCallEndpointsChanged(String, List, Session.Info) */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void onAvailableCallEndpointsChanged(Call activeCall,
+ Set<CallEndpoint> availableCallEndpoints) {
+ final String callId = mCallIdMapper.getCallId(activeCall);
+ if (callId != null && isServiceValid("onAvailableCallEndpointsChanged")) {
+ try {
+ logOutgoing("onAvailableCallEndpointsChanged %s", callId);
+ List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
+ mServiceInterface.onAvailableCallEndpointsChanged(callId, availableEndpoints,
+ Log.getExternalSession(TELECOM_ABBREVIATION));
+ } catch (RemoteException e) {
+ Log.d(this,
+ "Remote exception calling onAvailableCallEndpointsChanged");
+ }
+ }
+ }
+
+ /** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void onMuteStateChanged(Call activeCall, boolean isMuted) {
+ final String callId = mCallIdMapper.getCallId(activeCall);
+ if (callId != null && isServiceValid("onMuteStateChanged")) {
+ try {
+ logOutgoing("onMuteStateChanged %s %s", callId, isMuted);
+ mServiceInterface.onMuteStateChanged(callId, isMuted,
+ Log.getExternalSession(TELECOM_ABBREVIATION));
+ } catch (RemoteException e) {
+ Log.d(this, "Remote exception calling onMuteStateChanged");
+ }
+ }
+ }
+
/** @see IConnectionService#onUsingAlternativeUi(String, boolean, Session.Info) */
@VisibleForTesting
public void onUsingAlternativeUi(Call activeCall, boolean isUsingAlternativeUi) {
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 356121146..19691c1a2 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -16,8 +16,10 @@
package com.android.server.telecom;
+import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.os.UserHandle;
import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telecom.ParcelableConference;
@@ -389,12 +391,23 @@ public class CreateConnectionProcessor implements CreateConnectionResponse {
// current user.
// ONLY include phone accounts which are NOT self-managed; we will never consider a self
// managed phone account for placing an emergency call.
+ UserHandle userFromCall = mCall.getAssociatedUser();
List<PhoneAccount> allAccounts = mPhoneAccountRegistrar
- .getAllPhoneAccountsOfCurrentUser()
+ .getAllPhoneAccounts(userFromCall, false)
.stream()
.filter(act -> !act.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED))
.collect(Collectors.toList());
+ if (allAccounts.isEmpty()) {
+ // Try using phone accounts from other users to place the call (i.e. using an
+ // available work sim) given that the current user has the INTERACT_ACROSS_USERS
+ // permission.
+ allAccounts = mPhoneAccountRegistrar.getAllPhoneAccounts(userFromCall, true)
+ .stream()
+ .filter(act -> !act.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED))
+ .collect(Collectors.toList());
+ }
+
if (allAccounts.isEmpty() && mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY)) {
// If the list of phone accounts is empty at this point, it means Telephony hasn't
@@ -416,19 +429,28 @@ public class CreateConnectionProcessor implements CreateConnectionResponse {
// Get user preferred PA if it exists.
PhoneAccount preferredPA = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
preferredPAH);
- // Next, add all SIM phone accounts which can place emergency calls.
- sortSimPhoneAccountsForEmergency(allAccounts, preferredPA);
- // and pick the first one that can place emergency calls.
- for (PhoneAccount phoneAccount : allAccounts) {
- if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
- && phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
- PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
- Log.i(this, "Will try PSTN account %s for emergency", phoneAccountHandle);
- mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle,
- phoneAccountHandle));
- // Add only one emergency SIM PhoneAccount to the attempt list, telephony will
- // perform retries if the call fails.
- break;
+ if (mCall.isIncoming() && preferredPA != null) {
+ // The phone account for the incoming call should be used.
+ mAttemptRecords.add(new CallAttemptRecord(preferredPA.getAccountHandle(),
+ preferredPA.getAccountHandle()));
+ } else {
+ // Next, add all SIM phone accounts which can place emergency calls.
+ sortSimPhoneAccountsForEmergency(allAccounts, preferredPA);
+ Log.i(this, "The preferred PA is: %s", preferredPA);
+ // and pick the first one that can place emergency calls.
+ for (PhoneAccount phoneAccount : allAccounts) {
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
+ && phoneAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
+ Log.i(this, "Will try PSTN account %s for emergency",
+ phoneAccountHandle);
+ mAttemptRecords.add(new CallAttemptRecord(phoneAccountHandle,
+ phoneAccountHandle));
+ // Add only one emergency SIM PhoneAccount to the attempt list, telephony
+ // will perform retries if the call fails.
+ break;
+ }
}
}
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index a4a0242b3..dc7971554 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -160,6 +160,7 @@ public class DefaultDialerCache {
packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
packageIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
packageIntentFilter.addDataScheme("package");
+ packageIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
context.registerReceiverAsUser(mReceiver, UserHandle.ALL, packageIntentFilter, null, null);
IntentFilter bootIntentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
@@ -265,7 +266,7 @@ public class DefaultDialerCache {
if (packageName == null ||
Objects.equals(packageName, mCurrentDefaultDialerPerUser.get(userId))) {
String newDefaultDialer = refreshCacheForUser(userId);
- Log.i(LOG_TAG, "Refreshing default dialer for user %d: now %s",
+ Log.v(LOG_TAG, "Refreshing default dialer for user %d: now %s",
userId, newDefaultDialer);
}
}
diff --git a/src/com/android/server/telecom/DockManager.java b/src/com/android/server/telecom/DockManager.java
index dda5711fe..114672e22 100644
--- a/src/com/android/server/telecom/DockManager.java
+++ b/src/com/android/server/telecom/DockManager.java
@@ -76,6 +76,7 @@ public class DockManager {
// Register for misc other intent broadcasts.
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_DOCK_EVENT);
+ intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
context.registerReceiver(mReceiver, intentFilter);
}
diff --git a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
new file mode 100644
index 000000000..af79da36a
--- /dev/null
+++ b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
@@ -0,0 +1,438 @@
+/*
+ * 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 android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+
+import android.os.BugreportManager;
+import android.os.DropBoxManager;
+import android.provider.DeviceConfig;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telephony.TelephonyManager;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * The EmergencyCallDiagnosticsLogger monitors information required to diagnose potential outgoing
+ * ecall failures on the device. When a potential failure is detected, it calls a Telephony API to
+ * persist relevant information (dumpsys, logcat etc.) to the dropbox. This acts as a central place
+ * to determine when and what to collect.
+ *
+ * <p>When a bugreport is triggered, this module will read the dropbox entries and add them to the
+ * telecom dump.
+ */
+public class EmergencyCallDiagnosticLogger extends CallsManagerListenerBase
+ implements Call.Listener {
+
+ public static final int REPORT_REASON_RANGE_START = -1; //!!DO NOT CHANGE
+ public static final int REPORT_REASON_RANGE_END = 5; //increment this and add new reason above
+ public static final int COLLECTION_TYPE_BUGREPORT = 10;
+ public static final int COLLECTION_TYPE_TELECOM_STATE = 11;
+ public static final int COLLECTION_TYPE_TELEPHONY_STATE = 12;
+ public static final int COLLECTION_TYPE_LOGCAT_BUFFERS = 13;
+ private static final int REPORT_REASON_STUCK_CALL_DETECTED = 0;
+ private static final int REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY = 1;
+ private static final int REPORT_REASON_CALL_FAILED = 2;
+ private static final int REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED = 3;
+ private static final int REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE = 4;
+ private static final String DROPBOX_TAG = "ecall_diagnostic_data";
+ private static final String ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+ "enable_bugreport_collection_for_emergency_call_diagnostics";
+ private static final String ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+ "enable_telecom_dump_collection_for_emergency_call_diagnostics";
+
+ private static final String ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+ "enable_logcat_collection_for_emergency_call_diagnostics";
+ private static final String ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS =
+ "enable_telephony_dump_collection_for_emergency_call_diagnostics";
+
+ private static final String DUMPSYS_ARG_FOR_DIAGNOSTICS = "EmergencyDiagnostics";
+
+ // max text size to read from dropbox entry
+ private static final int DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY = 500000;
+ private static final String MAX_BYTES_PER_DROP_BOX_ENTRY = "max_bytes_per_dropbox_entry";
+ private static final int MAX_DROPBOX_ENTRIES_TO_DUMP = 6;
+
+ private final Timeouts.Adapter mTimeoutAdapter;
+ // This map holds all calls, but keeps pruning non-emergency calls when we can determine it
+ private final Map<Call, CallEventTimestamps> mEmergencyCallsMap = new ConcurrentHashMap<>(2);
+ private final DropBoxManager mDropBoxManager;
+ private final LocalLog mLocalLog = new LocalLog(10);
+ private final TelephonyManager mTelephonyManager;
+ private final BugreportManager mBugreportManager;
+ private final Executor mAsyncTaskExecutor;
+ private final ClockProxy mClockProxy;
+
+ public EmergencyCallDiagnosticLogger(
+ TelephonyManager tm,
+ BugreportManager brm,
+ Timeouts.Adapter timeoutAdapter, DropBoxManager dropBoxManager,
+ Executor asyncTaskExecutor, ClockProxy clockProxy) {
+ mTimeoutAdapter = timeoutAdapter;
+ mDropBoxManager = dropBoxManager;
+ mTelephonyManager = tm;
+ mBugreportManager = brm;
+ mAsyncTaskExecutor = asyncTaskExecutor;
+ mClockProxy = clockProxy;
+ }
+
+ // this calculates time from ACTIVE --> removed
+ private static long getCallTimeInActiveStateSec(CallEventTimestamps ts) {
+ if (ts.getCallActiveTime() == 0 || ts.getCallRemovedTime() == 0) {
+ return 0;
+ } else {
+ return (ts.getCallRemovedTime() - ts.getCallActiveTime()) / 1000;
+ }
+ }
+
+ // this calculates time from call created --> removed
+ private static long getTotalCallTimeSec(CallEventTimestamps ts) {
+ if (ts.getCallRemovedTime() == 0 || ts.getCallCreatedTime() == 0) {
+ return 0;
+ } else {
+ return (ts.getCallRemovedTime() - ts.getCallCreatedTime()) / 1000;
+ }
+ }
+
+ //determines what to collect based on fail reason
+ //if COLLECTION_TYPE_BUGREPORT is present in the returned list, then that
+ //should be the only collection type in the list
+ @VisibleForTesting
+ public static List<Integer> getDataCollectionTypes(int reason) {
+ switch (reason) {
+ case REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE:
+ return Arrays.asList(COLLECTION_TYPE_TELECOM_STATE);
+ case REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED:
+ return Arrays.asList(
+ COLLECTION_TYPE_TELECOM_STATE, COLLECTION_TYPE_TELEPHONY_STATE);
+ case REPORT_REASON_CALL_FAILED:
+ case REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY:
+ case REPORT_REASON_STUCK_CALL_DETECTED:
+ return Arrays.asList(
+ COLLECTION_TYPE_TELECOM_STATE,
+ COLLECTION_TYPE_TELEPHONY_STATE,
+ COLLECTION_TYPE_LOGCAT_BUFFERS);
+ default:
+ }
+ return new ArrayList<>();
+ }
+
+ private int getMaxBytesPerDropboxEntry() {
+ return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+ MAX_BYTES_PER_DROP_BOX_ENTRY, DEFAULT_MAX_READ_BYTES_PER_DROP_BOX_ENTRY);
+ }
+
+ @VisibleForTesting
+ public Map<Call, CallEventTimestamps> getEmergencyCallsMap() {
+ return mEmergencyCallsMap;
+ }
+
+ private void triggerDiagnosticsCollection(Call call, int reason) {
+ Log.i(this, "Triggering diagnostics for call %s reason: %d", call.getId(), reason);
+ List<Integer> dataCollectionTypes = getDataCollectionTypes(reason);
+ boolean invokeTelephonyPersistApi = false;
+ CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+ EmergencyCallDiagnosticParams dp =
+ new EmergencyCallDiagnosticParams();
+ for (Integer dataCollectionType : dataCollectionTypes) {
+ switch (dataCollectionType) {
+ case COLLECTION_TYPE_TELECOM_STATE:
+ if (isTelecomDumpCollectionEnabled()) {
+ dp.setTelecomDumpSysCollection(true);
+ invokeTelephonyPersistApi = true;
+ }
+ break;
+ case COLLECTION_TYPE_TELEPHONY_STATE:
+ if (isTelephonyDumpCollectionEnabled()) {
+ dp.setTelephonyDumpSysCollection(true);
+ invokeTelephonyPersistApi = true;
+ }
+ break;
+ case COLLECTION_TYPE_LOGCAT_BUFFERS:
+ if (isLogcatCollectionEnabled()) {
+ dp.setLogcatCollection(true, ts.getCallCreatedTime());
+ invokeTelephonyPersistApi = true;
+ }
+ break;
+ case COLLECTION_TYPE_BUGREPORT:
+ if (isBugreportCollectionEnabled()) {
+ mAsyncTaskExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ persistBugreport();
+ }
+ });
+ }
+ break;
+ default:
+ }
+ }
+ if (invokeTelephonyPersistApi) {
+ mAsyncTaskExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.i(this, "Requesting Telephony to persist data %s", dp.toString());
+ try {
+ mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp);
+ } catch (Exception e) {
+ Log.w(this,
+ "Exception while invoking "
+ + "Telephony#persistEmergencyCallDiagnosticData %s",
+ e.toString());
+ }
+ }
+ });
+ }
+ }
+
+ private boolean isBugreportCollectionEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_TELEPHONY,
+ ENABLE_BUGREPORT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+ false);
+ }
+
+ private boolean isTelecomDumpCollectionEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_TELEPHONY,
+ ENABLE_TELECOM_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+ true);
+ }
+
+ private boolean isLogcatCollectionEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_TELEPHONY,
+ ENABLE_LOGCAT_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+ true);
+ }
+
+ private boolean isTelephonyDumpCollectionEnabled() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_TELEPHONY,
+ ENABLE_TELEPHONY_DUMP_COLLECTION_FOR_EMERGENCY_CALL_DIAGNOSTICS,
+ true);
+ }
+
+ private void persistBugreport() {
+ if (isBugreportCollectionEnabled()) {
+ // TODO:
+ }
+ }
+
+ private boolean shouldTrackCall(Call call) {
+ return (call != null && call.isEmergencyCall() && call.isOutgoing());
+ }
+
+ public void reportStuckCall(Call call) {
+ if (shouldTrackCall(call)) {
+ Log.i(this, "Triggering diagnostics for stuck call %s", call.getId());
+ triggerDiagnosticsCollection(call, REPORT_REASON_STUCK_CALL_DETECTED);
+ call.removeListener(this);
+ mEmergencyCallsMap.remove(call);
+ }
+ }
+
+ @Override
+ public void onStartCreateConnection(Call call) {
+ if (shouldTrackCall(call)) {
+ long currentTime = mClockProxy.currentTimeMillis();
+ call.addListener(this);
+ Log.i(this, "Tracking call %s timestamp: %d", call.getId(), currentTime);
+ mEmergencyCallsMap.put(call, new CallEventTimestamps(currentTime));
+ }
+ }
+
+ @Override
+ public void onCreateConnectionFailed(Call call) {
+ if (shouldTrackCall(call)) {
+ Log.i(this, "Triggering diagnostics for call %s that was never added", call.getId());
+ triggerDiagnosticsCollection(call, REPORT_REASON_CALL_CREATED_BUT_NEVER_ADDED);
+ call.removeListener(this);
+ mEmergencyCallsMap.remove(call);
+ }
+ }
+
+ /**
+ * Override of {@link CallsManagerListenerBase} to track when calls are removed
+ *
+ * @param call the call
+ */
+ @Override
+ public void onCallRemoved(Call call) {
+ if (call != null && (mEmergencyCallsMap.get(call) != null)) {
+ call.removeListener(this);
+
+ CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+ long currentTime = mClockProxy.currentTimeMillis();
+ ts.setCallRemovedTime(currentTime);
+
+ maybeTriggerDiagnosticsCollection(call, ts);
+ mEmergencyCallsMap.remove(call);
+ }
+ }
+
+ // !NOTE!: this method should only be called after we get onCallRemoved
+ private void maybeTriggerDiagnosticsCollection(Call removedCall, CallEventTimestamps ts) {
+ Log.i(this, "Evaluating emergency call for diagnostic logging: %s", removedCall.getId());
+ boolean wentActive = (ts.getCallActiveTime() != 0);
+ long callActiveTimeSec = (wentActive ? getCallTimeInActiveStateSec(ts) : 0);
+ long timeSinceCallCreatedSec = getTotalCallTimeSec(ts);
+ int dc = removedCall.getDisconnectCause().getCode();
+
+ if (wentActive) {
+ if (callActiveTimeSec
+ < mTimeoutAdapter.getEmergencyCallActiveTimeThresholdMillis() / 1000) {
+ // call connected but did not go on for long
+ triggerDiagnosticsCollection(
+ removedCall, REPORT_REASON_SHORT_DURATION_AFTER_GOING_ACTIVE);
+ }
+ } else {
+
+ if (dc == DisconnectCause.LOCAL
+ && timeSinceCallCreatedSec
+ > mTimeoutAdapter.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()
+ / 1000) {
+ // call was disconnected by the user (but not immediately)
+ triggerDiagnosticsCollection(
+ removedCall, REPORT_REASON_INACTIVE_CALL_TERMINATED_BY_USER_AFTER_DELAY);
+ } else if (dc != DisconnectCause.LOCAL) {
+ // this can be a case for a full bugreport
+ triggerDiagnosticsCollection(removedCall, REPORT_REASON_CALL_FAILED);
+ }
+ }
+ }
+
+ /**
+ * Override of {@link com.android.server.telecom.CallsManager.CallsManagerListener} to track
+ * call state changes.
+ *
+ * @param call the call
+ * @param oldState its old state
+ * @param newState the new state
+ */
+ @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);
+ }
+ }
+ }
+
+ private void dumpDiagnosticDataFromDropbox(IndentingPrintWriter pw) {
+ pw.increaseIndent();
+ pw.println("PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
+ int totalEntriesDumped = 0;
+ long currentTime = mClockProxy.currentTimeMillis();
+ long entriesAfterTime =
+ currentTime - (mTimeoutAdapter.getDaysBackToSearchEmergencyDiagnosticEntries() * 24
+ * 60L * 60L * 1000L);
+ Log.i(this, "current time: %d entriesafter: %d", currentTime, entriesAfterTime);
+ DropBoxManager.Entry entry;
+ entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entriesAfterTime);
+ while (entry != null) {
+ Log.i(this, "found entry with ts: %d", entry.getTimeMillis());
+ String content[] = entry.getText(getMaxBytesPerDropboxEntry()).split(
+ System.lineSeparator());
+ long entryTime = entry.getTimeMillis();
+ if (content != null) {
+ pw.increaseIndent();
+ pw.println("------------BEGIN ENTRY (" + entryTime + ")--------");
+ for (String line : content) {
+ pw.println(line);
+ }
+ pw.println("--------END ENTRY--------");
+ pw.decreaseIndent();
+ totalEntriesDumped++;
+ }
+ entry = mDropBoxManager.getNextEntry(DROPBOX_TAG, entryTime);
+ if (totalEntriesDumped > MAX_DROPBOX_ENTRIES_TO_DUMP) {
+ /*
+ Since Emergency calls are a rare/once in a lifetime time occurrence for most users,
+ we should not be seeing too many entries. This code just guards against edge case
+ like load testing, b2b failures etc. We may accumulate a lot of dropbox entries in
+ such cases, but we limit to dumping only MAX_DROPBOX_ENTRIES_TO_DUMP in the
+ bugreport
+
+ The Dropbox API in its current state does not allow to query Entries in reverse
+ chronological order efficiently.
+ */
+
+ Log.i(this, "Skipping dump for remaining entries. dumped :%d", totalEntriesDumped);
+ break;
+ }
+ }
+ pw.println("END OF PERSISTED DIAGNOSTIC DATA FROM DROP BOX");
+ pw.decreaseIndent();
+ }
+
+ public void dump(IndentingPrintWriter pw, String[] args) {
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ if (args != null && args.length > 0 && args[0].equals(DUMPSYS_ARG_FOR_DIAGNOSTICS)) {
+ //dont read dropbox entries since this dump is triggered by telephony for diagnostics
+ Log.i(this, "skipped dumping diagnostic data");
+ return;
+ }
+ dumpDiagnosticDataFromDropbox(pw);
+ }
+
+ private static class CallEventTimestamps {
+
+ private final long mCallCreatedTime;
+ private long mCallActiveTime;
+ private long mCallRemovedTime;
+
+ public CallEventTimestamps(long createdTime) {
+ mCallCreatedTime = createdTime;
+ }
+
+ public long getCallActiveTime() {
+ return mCallActiveTime;
+ }
+
+ public void setCallActiveTime(long callActiveTime) {
+ this.mCallActiveTime = callActiveTime;
+ }
+
+ public long getCallCreatedTime() {
+ return mCallCreatedTime;
+ }
+
+ public long getCallRemovedTime() {
+ return mCallRemovedTime;
+ }
+
+ public void setCallRemovedTime(long callRemovedTime) {
+ this.mCallRemovedTime = callRemovedTime;
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index 5de4e5a85..5ab0e99d0 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -21,6 +21,8 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -34,9 +36,20 @@ public class EmergencyCallHelper {
private final DefaultDialerCache mDefaultDialerCache;
private final Timeouts.Adapter mTimeoutsAdapter;
private UserHandle mLocationPermissionGrantedToUser;
+ private PhoneAccountHandle mLastOutgoingEmergencyCallPAH;
+
+ //stores the original state of permissions that dialer had
private boolean mHadFineLocation = false;
private boolean mHadBackgroundLocation = false;
+
+ //stores whether we successfully granted the runtime permission
+ //This is stored so we don't unnecessarily revoke if the grant had failed with an exception.
+ //Else we will get an exception
+ private boolean mFineLocationGranted= false;
+ private boolean mBackgroundLocationGranted = false;
+
private long mLastEmergencyCallTimestampMillis;
+ private long mLastOutgoingEmergencyCallTimestampMillis;
@VisibleForTesting
public EmergencyCallHelper(
@@ -48,16 +61,18 @@ public class EmergencyCallHelper {
mTimeoutsAdapter = timeoutsAdapter;
}
- void maybeGrantTemporaryLocationPermission(Call call, UserHandle userHandle) {
+ @VisibleForTesting
+ public void maybeGrantTemporaryLocationPermission(Call call, UserHandle userHandle) {
if (shouldGrantTemporaryLocationPermission(call)) {
grantLocationPermission(userHandle);
}
if (call != null && call.isEmergencyCall()) {
- recordEmergencyCallTime();
+ recordEmergencyCall(call);
}
}
- void maybeRevokeTemporaryLocationPermission() {
+ @VisibleForTesting
+ public void maybeRevokeTemporaryLocationPermission() {
if (wasGrantedTemporaryLocationPermission()) {
revokeLocationPermission();
}
@@ -67,15 +82,44 @@ public class EmergencyCallHelper {
return mLastEmergencyCallTimestampMillis;
}
- private void recordEmergencyCallTime() {
- mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+ @VisibleForTesting
+ public void setLastOutgoingEmergencyCallTimestampMillis(long timestampMillis) {
+ mLastOutgoingEmergencyCallTimestampMillis = timestampMillis;
+ }
+
+ @VisibleForTesting
+ public void setLastOutgoingEmergencyCallPAH(PhoneAccountHandle accountHandle) {
+ mLastOutgoingEmergencyCallPAH = accountHandle;
}
- private boolean isInEmergencyCallbackWindow() {
- return System.currentTimeMillis() - getLastEmergencyCallTimeMillis()
+ @VisibleForTesting
+ public boolean isLastOutgoingEmergencyCallPAH(PhoneAccountHandle currentCallHandle) {
+ boolean ecbmActive = mLastOutgoingEmergencyCallPAH != null
+ && isInEmergencyCallbackWindow(mLastOutgoingEmergencyCallTimestampMillis)
+ && currentCallHandle != null
+ && currentCallHandle.equals(mLastOutgoingEmergencyCallPAH);
+ if (ecbmActive) {
+ Log.i(this, "ECBM is enabled for %s. The last recorded call timestamp was at %s",
+ currentCallHandle, mLastOutgoingEmergencyCallTimestampMillis);
+ }
+
+ return ecbmActive;
+ }
+
+ boolean isInEmergencyCallbackWindow(long lastEmergencyCallTimestampMillis) {
+ return System.currentTimeMillis() - lastEmergencyCallTimestampMillis
< mTimeoutsAdapter.getEmergencyCallbackWindowMillis(mContext.getContentResolver());
}
+ private void recordEmergencyCall(Call call) {
+ mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+ if (!call.isIncoming()) {
+ // ECBM is applicable to MO emergency calls
+ mLastOutgoingEmergencyCallTimestampMillis = mLastEmergencyCallTimestampMillis;
+ mLastOutgoingEmergencyCallPAH = call.getTargetPhoneAccount();
+ }
+ }
+
private boolean shouldGrantTemporaryLocationPermission(Call call) {
if (!mContext.getResources().getBoolean(R.bool.grant_location_permission_enabled)) {
Log.i(this, "ShouldGrantTemporaryLocationPermission, disabled by config");
@@ -85,7 +129,8 @@ public class EmergencyCallHelper {
Log.i(this, "ShouldGrantTemporaryLocationPermission, no call");
return false;
}
- if (!call.isEmergencyCall() && !isInEmergencyCallbackWindow()) {
+ if (!call.isEmergencyCall() && !isInEmergencyCallbackWindow(
+ getLastEmergencyCallTimeMillis())) {
Log.i(this, "ShouldGrantTemporaryLocationPermission, not emergency");
return false;
}
@@ -95,51 +140,65 @@ public class EmergencyCallHelper {
private void grantLocationPermission(UserHandle userHandle) {
String systemDialerPackage = mDefaultDialerCache.getSystemDialerApplication();
- Log.i(this, "Granting temporary location permission to " + systemDialerPackage
- + ", user: " + userHandle);
- try {
- boolean hadBackgroundLocation = hasBackgroundLocationPermission();
- boolean hadFineLocation = hasFineLocationPermission();
- if (hadBackgroundLocation && hadFineLocation) {
- Log.i(this, "Skipping location grant because the system dialer already"
- + " holds sufficient permissions");
- return;
- }
- if (!hadFineLocation) {
+ Log.i(this, "Attempting to grant temporary location permission to " + systemDialerPackage
+ + ", user: " + userHandle);
+
+ boolean hadBackgroundLocation = hasBackgroundLocationPermission();
+ boolean hadFineLocation = hasFineLocationPermission();
+ if (hadBackgroundLocation && hadFineLocation) {
+ Log.i(this, "Skipping location grant because the system dialer already"
+ + " holds sufficient permissions");
+ return;
+ }
+ mHadFineLocation = hadFineLocation;
+ mHadBackgroundLocation = hadBackgroundLocation;
+
+ if (!hadFineLocation) {
+ try {
mContext.getPackageManager().grantRuntimePermission(systemDialerPackage,
- Manifest.permission.ACCESS_FINE_LOCATION, userHandle);
+ Manifest.permission.ACCESS_FINE_LOCATION, userHandle);
+ recordFineLocationPermissionGrant(userHandle);
+ } catch (Exception e) {
+ Log.i(this, "Failed to grant ACCESS_FINE_LOCATION");
}
- if (!hadBackgroundLocation) {
+ }
+ if (!hadBackgroundLocation) {
+ try {
mContext.getPackageManager().grantRuntimePermission(systemDialerPackage,
- Manifest.permission.ACCESS_BACKGROUND_LOCATION, userHandle);
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION, userHandle);
+ recordBackgroundLocationPermissionGrant(userHandle);
+ } catch (Exception e) {
+ Log.i(this, "Failed to grant ACCESS_BACKGROUND_LOCATION");
}
- mHadFineLocation = hadFineLocation;
- mHadBackgroundLocation = hadBackgroundLocation;
- recordPermissionGrant(userHandle);
- } catch (Exception e) {
- Log.e(this, e, "Failed to grant location permissions to " + systemDialerPackage
- + ", user: " + userHandle);
}
}
private void revokeLocationPermission() {
String systemDialerPackage = mDefaultDialerCache.getSystemDialerApplication();
Log.i(this, "Revoking temporary location permission from " + systemDialerPackage
- + ", user: " + mLocationPermissionGrantedToUser);
+ + ", user: " + mLocationPermissionGrantedToUser);
UserHandle userHandle = mLocationPermissionGrantedToUser;
+
try {
- if (!mHadFineLocation) {
+ if (!mHadFineLocation && mFineLocationGranted) {
mContext.getPackageManager().revokeRuntimePermission(systemDialerPackage,
- Manifest.permission.ACCESS_FINE_LOCATION, userHandle);
+ Manifest.permission.ACCESS_FINE_LOCATION, userHandle);
}
- if (!mHadBackgroundLocation) {
+ } catch (Exception e) {
+ Log.e(this, e, "Failed to revoke location permission from " + systemDialerPackage
+ + ", user: " + userHandle);
+ }
+
+ try {
+ if (!mHadBackgroundLocation && mBackgroundLocationGranted) {
mContext.getPackageManager().revokeRuntimePermission(systemDialerPackage,
- Manifest.permission.ACCESS_BACKGROUND_LOCATION, userHandle);
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION, userHandle);
}
} catch (Exception e) {
Log.e(this, e, "Failed to revoke location permission from " + systemDialerPackage
- + ", user: " + userHandle);
+ + ", user: " + userHandle);
}
+
clearPermissionGrant();
}
@@ -157,8 +216,14 @@ public class EmergencyCallHelper {
== PackageManager.PERMISSION_GRANTED;
}
- private void recordPermissionGrant(UserHandle userHandle) {
+ private void recordBackgroundLocationPermissionGrant(UserHandle userHandle) {
+ mLocationPermissionGrantedToUser = userHandle;
+ mBackgroundLocationGranted = true;
+ }
+
+ private void recordFineLocationPermissionGrant(UserHandle userHandle) {
mLocationPermissionGrantedToUser = userHandle;
+ mFineLocationGranted = true;
}
private boolean wasGrantedTemporaryLocationPermission() {
@@ -169,5 +234,7 @@ public class EmergencyCallHelper {
mLocationPermissionGrantedToUser = null;
mHadBackgroundLocation = false;
mHadFineLocation = false;
+ mBackgroundLocationGranted = false;
+ mFineLocationGranted = false;
}
}
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index b1471c297..7458f546b 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -67,6 +67,11 @@ public class HeadsetMediaButton extends CallsManagerListenerBase {
mMediaSession.setActive(active);
}
+ @Override
+ public void setCallback(MediaSession.Callback callback) {
+ mMediaSession.setCallback(callback);
+ }
+
/**
* Gets the underlying {@link MediaSession} active status.
* @return {@code true} if active, {@code false} otherwise.
@@ -84,6 +89,7 @@ public class HeadsetMediaButton extends CallsManagerListenerBase {
*/
public interface MediaSessionAdapter {
void setActive(boolean active);
+ void setCallback(MediaSession.Callback callback);
boolean isActive();
}
@@ -165,6 +171,8 @@ public class HeadsetMediaButton extends CallsManagerListenerBase {
mCallsManager = callsManager;
mLock = lock;
mSession = adapter;
+
+ adapter.setCallback(mSessionCallback);
}
/**
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 0fda5f86d..9ce10bd7d 100755
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -19,6 +19,8 @@ package com.android.server.telecom;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.telecom.CallEndpoint;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
@@ -396,6 +398,23 @@ class InCallAdapter extends IInCallAdapter.Stub {
}
@Override
+ public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+ try {
+ Log.startSession(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, mOwnerPackageAbbreviation);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallsManager.requestCallEndpointChange(endpoint, callback);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
public void enterBackgroundAudioProcessing(String callId) {
try {
Log.startSession(LogUtils.Sessions.ICA_ENTER_AUDIO_PROCESSING,
@@ -602,7 +621,9 @@ class InCallAdapter extends IInCallAdapter.Stub {
synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
- call.putExtras(Call.SOURCE_INCALL_SERVICE, extras);
+ // Make sure to identify the ICS that originated the extras change so that
+ // InCallController can propagate these out to other ICSes.
+ call.putInCallServiceExtras(extras, mOwnerPackageName);
} else {
Log.w(this, "putExtras, unknown call id: %s", callId);
}
@@ -674,7 +695,7 @@ class InCallAdapter extends IInCallAdapter.Stub {
@Override
public void sendRttRequest(String callId) {
try {
- Log.startSession("ICA.sRR");
+ Log.startSession("ICA.sRR", mOwnerPackageAbbreviation);
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -696,7 +717,7 @@ class InCallAdapter extends IInCallAdapter.Stub {
@Override
public void respondToRttRequest(String callId, int id, boolean accept) {
try {
- Log.startSession("ICA.rTRR");
+ Log.startSession("ICA.rTRR", mOwnerPackageAbbreviation);
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -718,7 +739,7 @@ class InCallAdapter extends IInCallAdapter.Stub {
@Override
public void stopRtt(String callId) {
try {
- Log.startSession("ICA.sRTT");
+ Log.startSession("ICA.sRTT", mOwnerPackageAbbreviation);
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -740,11 +761,16 @@ class InCallAdapter extends IInCallAdapter.Stub {
@Override
public void setRttMode(String callId, int mode) {
try {
- Log.startSession("ICA.sRM");
+ Log.startSession("ICA.sRM", mOwnerPackageAbbreviation);
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- // TODO
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setRttMode(mode);
+ } else {
+ Log.w(this, "setRttMode(): call %s not found", callId);
+ }
}
} finally {
Binder.restoreCallingIdentity(token);
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index c469308c7..598664e0f 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -23,11 +23,8 @@ import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
-import android.app.compat.CompatChanges;
import android.app.Notification;
import android.app.NotificationManager;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -41,7 +38,6 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.hardware.SensorPrivacyManager;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -51,7 +47,9 @@ 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;
import android.telecom.InCallService;
import android.telecom.Log;
@@ -78,6 +76,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -91,6 +90,21 @@ public class InCallController extends CallsManagerListenerBase implements
AppOpsManager.OnOpActiveChangedListener {
public static final String NOTIFICATION_TAG = InCallController.class.getSimpleName();
public static final int IN_CALL_SERVICE_NOTIFICATION_ID = 3;
+ private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+
+ /**
+ * Anomaly Report UUIDs and corresponding error descriptions specific to InCallController.
+ */
+ public static final UUID SET_IN_CALL_ADAPTER_ERROR_UUID =
+ 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.";
+
+ @VisibleForTesting
+ public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+ mAnomalyReporter = mAnomalyReporterAdapter;
+ }
+
public class InCallServiceConnection {
/**
* Indicates that a call to {@link #connect(Call)} has succeeded and resulted in a
@@ -133,16 +147,20 @@ public class InCallController extends CallsManagerListenerBase implements
private long mBindingStartTime;
private long mDisconnectTime;
+ private boolean mHasCrossUserOrProfilePerm;
+
public InCallServiceInfo(ComponentName componentName,
boolean isExternalCallsSupported,
boolean isSelfManageCallsSupported,
- int type) {
+ int type, boolean hasCrossUserOrProfilePerm) {
mComponentName = componentName;
mIsExternalCallsSupported = isExternalCallsSupported;
mIsSelfManagedCallsSupported = isSelfManageCallsSupported;
mType = type;
+ mHasCrossUserOrProfilePerm = hasCrossUserOrProfilePerm;
}
+ public boolean hasCrossUserOrProfilePermission() { return mHasCrossUserOrProfilePerm; }
public ComponentName getComponentName() {
return mComponentName;
}
@@ -279,12 +297,25 @@ public class InCallController extends CallsManagerListenerBase implements
private boolean mIsNullBinding = false;
private NotificationManager mNotificationManager;
+ //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;
+
public InCallServiceBindingConnection(InCallServiceInfo info) {
mInCallServiceInfo = info;
+ mUserHandleToUseForBinding = null;
+ }
+
+ public InCallServiceBindingConnection(InCallServiceInfo info,
+ UserHandle userHandleToUseForBinding) {
+ mInCallServiceInfo = info;
+ mUserHandleToUseForBinding = userHandleToUseForBinding;
}
@Override
public int connect(Call call) {
+ UserHandle userFromCall = getUserFromCall(call);
+
if (mIsConnected) {
Log.addEvent(call, LogUtils.Events.INFO, "Already connected, ignoring request: "
+ mInCallServiceInfo);
@@ -294,7 +325,7 @@ public class InCallController extends CallsManagerListenerBase implements
// Notify this new added call
sendCallToService(call, mInCallServiceInfo,
- mInCallServices.get(mInCallServiceInfo));
+ mInCallServices.get(userFromCall).get(mInCallServiceInfo));
}
return CONNECTION_SUCCEEDED;
}
@@ -320,11 +351,35 @@ public class InCallController extends CallsManagerListenerBase implements
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);
+ // 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
+ // need to ensure that we bind to the CURRENT_USER in this case, as the work user would
+ // 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) && (
+ mUserHandleToUseForBinding != null)) {
+ //guarding change for non-UI/carmode-UI services which may not be present for
+ // work profile.
+ //In this case, we use the parent user handle. (This also seems to be more
+ // accurate that USER_CURRENT since we queried/discovered the packages using the
+ // parent handle)
+ if (mInCallServiceInfo.hasCrossUserOrProfilePermission()) {
+ userToBind = mUserHandleToUseForBinding;
+ } else {
+ Log.i(this,
+ "service does not have INTERACT_ACROSS_PROFILES or "
+ + "INTERACT_ACROSS_USERS permission");
+ }
+ }
+ Log.i(this, "using user id: %s for binding. User from Call is: %s", userToBind,
+ userFromCall);
if (!mContext.bindServiceAsUser(intent, mServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
- | Context.BIND_SCHEDULE_LIKE_TOP_APP,
- UserHandle.CURRENT)) {
+ | Context.BIND_SCHEDULE_LIKE_TOP_APP, userToBind)) {
Log.w(this, "Failed to connect.");
mIsConnected = false;
}
@@ -345,6 +400,7 @@ public class InCallController extends CallsManagerListenerBase implements
@Override
public void disconnect() {
if (mIsConnected) {
+ UserHandle userFromCall = getUserFromCall(mCall);
mInCallServiceInfo.setDisconnectTime(mClockProxy.elapsedRealtime());
Log.i(InCallController.this, "ICSBC#disconnect: unbinding after %s ms;"
+ "%s. isCrashed: %s", mInCallServiceInfo.mDisconnectTime
@@ -357,7 +413,7 @@ public class InCallController extends CallsManagerListenerBase implements
// Non-UI InCallServices are allowed to return null from onBind if they don't
// want to handle calls at the moment, so don't report them to the user as
// crashed.
- sendCrashedInCallServiceNotification(packageName);
+ sendCrashedInCallServiceNotification(packageName, userFromCall);
}
if (mCall != null) {
mCall.getAnalytics().addInCallService(
@@ -368,7 +424,7 @@ public class InCallController extends CallsManagerListenerBase implements
updateCallTracking(mCall, mInCallServiceInfo, false /* isAdd */);
}
- InCallController.this.onDisconnected(mInCallServiceInfo);
+ InCallController.this.onDisconnected(mInCallServiceInfo, userFromCall);
} else {
Log.i(InCallController.this, "ICSBC#disconnect: already disconnected; %s",
mInCallServiceInfo);
@@ -394,7 +450,8 @@ public class InCallController extends CallsManagerListenerBase implements
protected void onConnected(IBinder service) {
boolean shouldRemainConnected =
- InCallController.this.onConnected(mInCallServiceInfo, service);
+ InCallController.this.onConnected(mInCallServiceInfo, service,
+ getUserFromCall(mCall));
if (!shouldRemainConnected) {
// Sometimes we can opt to disconnect for certain reasons, like if the
// InCallService rejected our initialization step, or the calls went away
@@ -405,11 +462,25 @@ public class InCallController extends CallsManagerListenerBase implements
}
protected void onDisconnected() {
- InCallController.this.onDisconnected(mInCallServiceInfo);
+ boolean shouldReconnect = mIsConnected;
+ InCallController.this.onDisconnected(mInCallServiceInfo, getUserFromCall(mCall));
disconnect(); // Unbind explicitly if we get disconnected.
if (mListener != null) {
mListener.onDisconnect(InCallServiceBindingConnection.this, mCall);
}
+ // Check if we are expected to reconnect
+ if (shouldReconnect && shouldHandleReconnect()) {
+ connect(mCall); // reconnect
+ }
+ }
+
+ private boolean shouldHandleReconnect() {
+ int serviceType = mInCallServiceInfo.getType();
+ boolean nonUI = (serviceType == IN_CALL_SERVICE_TYPE_NON_UI)
+ || (serviceType == IN_CALL_SERVICE_TYPE_COMPANION);
+ boolean carModeUI = (serviceType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
+
+ return carModeUI || (nonUI && !mIsNullBinding);
}
}
@@ -462,9 +533,9 @@ public class InCallController extends CallsManagerListenerBase implements
// Could not connect to child, stop proxying.
mIsProxying = false;
}
-
+ UserHandle userFromCall = getUserFromCall(call);
mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
- mCallsManager.getCurrentUserHandle());
+ userFromCall);
if (call != null && call.isIncoming()
&& mEmergencyCallHelper.getLastEmergencyCallTimeMillis() > 0) {
@@ -472,7 +543,7 @@ public class InCallController extends CallsManagerListenerBase implements
Bundle extras = new Bundle();
extras.putLong(android.telecom.Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS,
mEmergencyCallHelper.getLastEmergencyCallTimeMillis());
- call.putExtras(Call.SOURCE_CONNECTION_SERVICE, extras);
+ call.putConnectionServiceExtras(extras);
}
// If we are here, we didn't or could not connect to child. So lets connect ourselves.
@@ -612,13 +683,13 @@ public class InCallController extends CallsManagerListenerBase implements
*
* @param packageName The package name of the car mode app.
*/
- public synchronized void changeCarModeApp(String packageName) {
+ public synchronized void changeCarModeApp(String packageName, UserHandle userHandle) {
Log.i(this, "changeCarModeApp: isCarModeNow=" + mIsCarMode);
InCallServiceInfo currentConnectionInfo = mCurrentConnection == null ? null
: mCurrentConnection.getInfo();
InCallServiceInfo carModeConnectionInfo =
- getInCallServiceComponent(packageName,
+ getInCallServiceComponent(userHandle, packageName,
IN_CALL_SERVICE_TYPE_CAR_MODE_UI, true /* ignoreDisabed */);
if (!Objects.equals(currentConnectionInfo, carModeConnectionInfo)
@@ -630,7 +701,7 @@ public class InCallController extends CallsManagerListenerBase implements
}
mCarModeConnection = mCurrentConnection =
- new InCallServiceBindingConnection(carModeConnectionInfo);
+ new InCallServiceBindingConnection(carModeConnectionInfo, userHandle);
mIsCarMode = true;
int result = mCurrentConnection.connect(null);
@@ -770,6 +841,9 @@ public class InCallController extends CallsManagerListenerBase implements
}
Call callToConnectWith = mCallIdMapper.getCalls().iterator().next();
for (InCallServiceBindingConnection newConnection : newConnections) {
+ // Ensure we track the new sub-connection so that when we later disconnect we will
+ // be able to disconnect it.
+ mSubConnections.add(newConnection);
newConnection.connect(callToConnectWith);
}
}
@@ -787,7 +861,7 @@ public class InCallController extends CallsManagerListenerBase implements
@Override
public void onConnectionPropertiesChanged(Call call, boolean didRttChange) {
- updateCall(call, false /* includeVideoProvider */, didRttChange);
+ updateCall(call, false /* includeVideoProvider */, didRttChange, null);
}
@Override
@@ -797,7 +871,7 @@ public class InCallController extends CallsManagerListenerBase implements
@Override
public void onVideoCallProviderChanged(Call call) {
- updateCall(call, true /* videoProviderChanged */, false);
+ updateCall(call, true /* videoProviderChanged */, false, null);
}
@Override
@@ -805,25 +879,35 @@ public class InCallController extends CallsManagerListenerBase implements
updateCall(call);
}
+ @Override
+ public void onCallerInfoChanged(Call call) {
+ updateCall(call);
+ }
+
/**
* Listens for changes to extras reported by a Telecom {@link Call}.
*
* Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
- * so we will only trigger an update of the call information if the source of the extras
- * change was a {@link ConnectionService}.
+ * so we will only trigger an update of the call information if the source of the
+ * extras change was a {@link ConnectionService}.
*
- * @param call The call.
- * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
+ * @param call The call.
+ * @param source The source of the extras change
+ * ({@link Call#SOURCE_CONNECTION_SERVICE} or
* {@link Call#SOURCE_INCALL_SERVICE}).
* @param extras The extras.
*/
@Override
- public void onExtrasChanged(Call call, int source, Bundle extras) {
- // Do not inform InCallServices of changes which originated there.
- if (source == Call.SOURCE_INCALL_SERVICE) {
- return;
+ public void onExtrasChanged(Call call, int source, Bundle extras,
+ String requestingPackageName) {
+ if (source == Call.SOURCE_CONNECTION_SERVICE) {
+ updateCall(call);
+ } else if (source == Call.SOURCE_INCALL_SERVICE && requestingPackageName != null) {
+ // If the change originated from another InCallService, we'll propagate the change
+ // to all other InCallServices running, EXCEPT the one who made the original change.
+ updateCall(call, false /* videoProviderChanged */, false /* rttInfoChanged */,
+ requestingPackageName);
}
- updateCall(call);
}
/**
@@ -894,7 +978,7 @@ public class InCallController extends CallsManagerListenerBase implements
@Override
public void onRttInitiationFailure(Call call, int reason) {
notifyRttInitiationFailure(call, reason);
- updateCall(call, false, true);
+ updateCall(call, false, true, null);
}
@Override
@@ -909,31 +993,137 @@ public class InCallController extends CallsManagerListenerBase implements
}
};
+ private UserHandle findChildManagedProfileUser(UserHandle parent, UserManager um) {
+ UserHandle childManagedProfileUser = null;
+
+ //find child managed profile user (if any)
+ List<UserHandle> allUsers = um.getAllProfiles();
+ for (UserHandle u : allUsers) {
+ if ((um.getProfileParent(u) != null) && (um.getProfileParent(u).equals(parent))
+ && um.isManagedProfile(u.getIdentifier())) {
+ //found managed profile child
+ Log.i(this,
+ "Child managed profile user found: " + u.getIdentifier());
+ childManagedProfileUser = u;
+ break;
+ }
+ }
+ return childManagedProfileUser;
+ }
private BroadcastReceiver mPackageChangedReceiver = new BroadcastReceiver() {
+ private List<InCallController.InCallServiceInfo> getNonUiInCallServiceInfoList(
+ Intent intent, UserHandle userHandle) {
+ String changedPackage = intent.getData().getSchemeSpecificPart();
+ List<InCallController.InCallServiceInfo> inCallServiceInfoList =
+ Arrays.stream(intent.getStringArrayExtra(
+ Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST))
+ .map((className) ->
+ ComponentName.createRelative(changedPackage,
+ className))
+ .filter(mKnownNonUiInCallServices::contains)
+ .flatMap(componentName -> getInCallServiceComponents(
+ userHandle, componentName,
+ IN_CALL_SERVICE_TYPE_NON_UI).stream())
+ .collect(Collectors.toList());
+ return ((inCallServiceInfoList != null) ? inCallServiceInfoList : new ArrayList<>());
+ }
+
+ //Here we query components using the userHandle. We then also query components using the
+ //parent userHandle (if any) while removing duplicates. For non-dup components found using
+ //parent userHandle, we use the overloaded InCallServiceBindingConnection constructor.
+ @SuppressWarnings("ReturnValueIgnored")
+ private List<InCallServiceBindingConnection> getNonUiInCallServiceBindingConnectionList(
+ Intent intent, @NonNull UserHandle userHandle, UserHandle parentUserHandle) {
+ List<InCallServiceBindingConnection> result = new ArrayList<>();
+ List<InCallController.InCallServiceInfo> serviceInfoListForParent = new ArrayList<>();
+
+ //query and add components for the child
+ List<InCallController.InCallServiceInfo> serviceInfoListForUser =
+ getNonUiInCallServiceInfoList(intent, userHandle);
+
+ //if user has a parent, get components for parents
+ if (parentUserHandle != null) {
+ serviceInfoListForParent = getNonUiInCallServiceInfoList(intent, parentUserHandle);
+ }
+
+ serviceInfoListForUser
+ .stream()
+ .map(InCallServiceBindingConnection::new)
+ .collect(Collectors.toCollection(() -> result));
+
+ serviceInfoListForParent
+ .stream()
+ .filter((e) -> !(serviceInfoListForUser.contains(e)))
+ .map((serviceinfo) -> new InCallServiceBindingConnection(serviceinfo,
+ parentUserHandle))
+ .collect(Collectors.toCollection(() -> result));
+
+ return result;
+ }
+
@Override
public void onReceive(Context context, Intent intent) {
Log.startSession("ICC.pCR");
+ UserManager um = mContext.getSystemService(UserManager.class);
try {
if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
synchronized (mLock) {
- String changedPackage = intent.getData().getSchemeSpecificPart();
- List<InCallServiceBindingConnection> componentsToBind =
- Arrays.stream(intent.getStringArrayExtra(
- Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST))
- .map((className) ->
- ComponentName.createRelative(changedPackage,
- className))
- .filter(mKnownNonUiInCallServices::contains)
- .flatMap(componentName -> getInCallServiceComponents(
- componentName,
- IN_CALL_SERVICE_TYPE_NON_UI).stream())
- .map(InCallServiceBindingConnection::new)
- .collect(Collectors.toList());
-
- if (mNonUIInCallServiceConnections != null) {
- mNonUIInCallServiceConnections.addConnections(componentsToBind);
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+ UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+ boolean isManagedProfile = um.isManagedProfile(userHandle.getIdentifier());
+
+ /*
+ There are two possibilities here:
+ 1) We get a work-profile/managed userHandle. In this case we need to check
+ if there are any ongoing calls for that user. If yes, then process further
+ by querying component using this user handle (also bindAsUser using this
+ handle). Else safely ignore it.
+ OR
+ 2) We get the primary/non-managed userHandle. In this case, we have two
+ sub-cases to handle:
+ a) If there are ongoing calls for this user, query components
+ using this user and addConnections
+ b) If there are ongoing calls for the child of this user, we
+ also addConnections to that child (but invoke bindAsUser later
+ with the parent handle).
+
+ */
+
+ UserHandle childManagedProfileUser = findChildManagedProfileUser(
+ userHandle, um);
+ boolean isUserKeyPresent = mNonUIInCallServiceConnections.containsKey(
+ userHandle);
+ boolean isChildUserKeyPresent = (childManagedProfileUser == null) ? false
+ : mNonUIInCallServiceConnections.containsKey(
+ childManagedProfileUser);
+ List<InCallServiceBindingConnection> componentsToBindForUser = null;
+ List<InCallServiceBindingConnection> componentsToBindForChild = null;
+
+ if(isUserKeyPresent) {
+ componentsToBindForUser =
+ getNonUiInCallServiceBindingConnectionList(intent,
+ userHandle, null);
}
+ if (isChildUserKeyPresent) {
+ componentsToBindForChild =
+ getNonUiInCallServiceBindingConnectionList(intent,
+ childManagedProfileUser, userHandle);
+ }
+
+ Log.i(this,
+ "isUserKeyPresent:%b isChildKeyPresent:%b isManagedProfile:%b "
+ + "user:%d",
+ isUserKeyPresent, isChildUserKeyPresent, isManagedProfile,
+ userHandle.getIdentifier());
+ if (isUserKeyPresent && componentsToBindForUser != null) {
+ mNonUIInCallServiceConnections.get(userHandle).
+ addConnections(componentsToBindForUser);
+ }
+ if (isChildUserKeyPresent && componentsToBindForChild != null) {
+ mNonUIInCallServiceConnections.get(childManagedProfileUser).
+ addConnections(componentsToBindForChild);
+ }
// If the current car mode app become enabled from disabled, update
// the connection to binding
updateCarModeForConnections();
@@ -988,7 +1178,8 @@ public class InCallController extends CallsManagerListenerBase implements
CallState.DISCONNECTING };
/** The in-call app implementations, see {@link IInCallService}. */
- private final Map<InCallServiceInfo, IInCallService> mInCallServices = new ArrayMap<>();
+ private final Map<UserHandle, Map<InCallServiceInfo, IInCallService>>
+ mInCallServices = new ArrayMap<>();
private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
@@ -1002,8 +1193,10 @@ public class InCallController extends CallsManagerListenerBase implements
private final DefaultDialerCache mDefaultDialerCache;
private final EmergencyCallHelper mEmergencyCallHelper;
private final Handler mHandler = new Handler(Looper.getMainLooper());
- private CarSwappingInCallServiceConnection mInCallServiceConnection;
- private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections;
+ private final Map<UserHandle, CarSwappingInCallServiceConnection>
+ mInCallServiceConnections = new ArrayMap<>();
+ private final Map<UserHandle, NonUIInCallServiceConnectionCollection>
+ mNonUIInCallServiceConnections = new ArrayMap<>();
private final ClockProxy mClockProxy;
private final IBinder mToken = new Binder();
@@ -1062,7 +1255,9 @@ public class InCallController extends CallsManagerListenerBase implements
mSystemStateHelper.addListener(mSystemStateListener);
mClockProxy = clockProxy;
restrictPhoneCallOps();
- mContext.registerReceiver(mUserAddedReceiver, new IntentFilter(Intent.ACTION_USER_ADDED));
+ IntentFilter userAddedFilter = new IntentFilter(Intent.ACTION_USER_ADDED);
+ userAddedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ mContext.registerReceiver(mUserAddedReceiver, userAddedFilter);
}
private void restrictPhoneCallOps() {
@@ -1136,59 +1331,71 @@ public class InCallController extends CallsManagerListenerBase implements
@Override
public void onCallAdded(Call call) {
- if (!isBoundAndConnectedToServices()) {
+ UserHandle userFromCall = getUserFromCall(call);
+
+ Log.i(this, "onCallAdded: %s", call);
+ // 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);
} else {
+ InCallServiceConnection inCallServiceConnection =
+ mInCallServiceConnections.get(userFromCall);
+
// We are bound, and we are connected.
- adjustServiceBindingsForEmergency();
+ adjustServiceBindingsForEmergency(userFromCall);
// This is in case an emergency call is added while there is an existing call.
mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
- mCallsManager.getCurrentUserHandle());
+ userFromCall);
- Log.i(this, "onCallAdded: %s", call);
- // Track the call if we don't already know about it.
- addCall(call);
-
- Log.i(this, "mInCallServiceConnection isConnected=%b",
- mInCallServiceConnection.isConnected());
+ if (inCallServiceConnection != null) {
+ Log.i(this, "mInCallServiceConnection isConnected=%b",
+ inCallServiceConnection.isConnected());
+ }
List<ComponentName> componentsUpdated = new ArrayList<>();
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
- InCallServiceInfo info = entry.getKey();
+ if (mInCallServices.containsKey(userFromCall)) {
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+ get(userFromCall).entrySet()) {
+ InCallServiceInfo info = entry.getKey();
- if (call.isExternalCall() && !info.isExternalCallsSupported()) {
- continue;
- }
+ if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+ continue;
+ }
- if (call.isSelfManaged() && (!call.visibleToInCallService()
- || !info.isSelfManagedCallsSupported())) {
- continue;
- }
+ 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 (mInCallServiceConnection != null) {
- includeRttCall = info.equals(mInCallServiceConnection.getInfo());
- }
+ // 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();
+ 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) {
+ 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 components: %s", componentsUpdated);
}
- Log.i(this, "Call added to components: %s", componentsUpdated);
}
}
@@ -1204,7 +1411,7 @@ public class InCallController extends CallsManagerListenerBase implements
public void loggedRun() {
// Check again to make sure there are no active calls.
if (mCallsManager.getCalls().isEmpty()) {
- unbindFromServices();
+ unbindFromServices(getUserFromCall(call));
mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
}
@@ -1227,10 +1434,12 @@ public class InCallController extends CallsManagerListenerBase implements
Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
List<ComponentName> componentsUpdated = new ArrayList<>();
- if (!isExternalCall) {
+ UserHandle userFromCall = getUserFromCall(call);
+ if (!isExternalCall && mInCallServices.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.entrySet()) {
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+ get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
if (info.isExternalCallsSupported()) {
@@ -1248,7 +1457,8 @@ public class InCallController extends CallsManagerListenerBase implements
IInCallService inCallService = entry.getValue();
// Only send the RTT call if it's a UI in-call service
- boolean includeRttCall = info.equals(mInCallServiceConnection.getInfo());
+ boolean includeRttCall = info.equals(mInCallServiceConnections.
+ get(userFromCall).getInfo());
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
@@ -1267,35 +1477,38 @@ public class InCallController extends CallsManagerListenerBase implements
// 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);
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
- InCallServiceInfo info = entry.getKey();
- if (info.isExternalCallsSupported()) {
- // For InCallServices which support external calls, we do not need to remove
- // the call.
- continue;
- }
+ if (mInCallServices.containsKey(userFromCall)) {
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+ get(userFromCall).entrySet()) {
+ InCallServiceInfo info = entry.getKey();
+ if (info.isExternalCallsSupported()) {
+ // For InCallServices which support external calls, we do not need to remove
+ // the call.
+ continue;
+ }
- componentsUpdated.add(info.getComponentName());
- IInCallService inCallService = entry.getValue();
+ componentsUpdated.add(info.getComponentName());
+ IInCallService inCallService = entry.getValue();
- ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
- call,
- false /* includeVideoProvider */,
- mCallsManager.getPhoneAccountRegistrar(),
- false /* supportsExternalCalls */,
- android.telecom.Call.STATE_DISCONNECTED /* overrideState */,
- false /* includeRttCall */,
- info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
- || info.getType() == IN_CALL_SERVICE_TYPE_NON_UI
- );
+ ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
+ call,
+ false /* includeVideoProvider */,
+ mCallsManager.getPhoneAccountRegistrar(),
+ false /* supportsExternalCalls */,
+ android.telecom.Call.STATE_DISCONNECTED /* overrideState */,
+ false /* includeRttCall */,
+ info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
+ || info.getType() == IN_CALL_SERVICE_TYPE_NON_UI
+ );
- try {
- inCallService.updateCall(
- sanitizeParcelableCallForService(info, parcelableCall));
- } catch (RemoteException ignored) {
+ try {
+ inCallService.updateCall(
+ sanitizeParcelableCallForService(info, parcelableCall));
+ } catch (RemoteException ignored) {
+ }
}
+ Log.i(this, "External call removed from components: %s", componentsUpdated);
}
- Log.i(this, "External call removed from components: %s", componentsUpdated);
}
maybeTrackMicrophoneUse(isMuted());
}
@@ -1321,12 +1534,63 @@ public class InCallController extends CallsManagerListenerBase implements
Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
newCallAudioState);
maybeTrackMicrophoneUse(newCallAudioState.isMuted());
- for (IInCallService inCallService : mInCallServices.values()) {
- try {
- inCallService.onCallAudioStateChanged(newCallAudioState);
- } catch (RemoteException ignored) {
+ mInCallServices.values().forEach(inCallServices -> {
+ for (IInCallService inCallService : inCallServices.values()) {
+ try {
+ inCallService.onCallAudioStateChanged(newCallAudioState);
+ } catch (RemoteException ignored) {
+ }
}
- }
+ });
+ }
+ }
+
+ @Override
+ public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+ if (!mInCallServices.isEmpty()) {
+ Log.i(this, "Calling onCallEndpointChanged");
+ mInCallServices.values().forEach(inCallServices -> {
+ for (IInCallService inCallService : inCallServices.values()) {
+ try {
+ inCallService.onCallEndpointChanged(callEndpoint);
+ } catch (RemoteException ignored) {
+ Log.d(this, "Remote exception calling onCallEndpointChanged");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
+ if (!mInCallServices.isEmpty()) {
+ Log.i(this, "Calling onAvailableCallEndpointsChanged");
+ List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
+ mInCallServices.values().forEach(inCallServices -> {
+ for (IInCallService inCallService : inCallServices.values()) {
+ try {
+ inCallService.onAvailableCallEndpointsChanged(availableEndpoints);
+ } catch (RemoteException ignored) {
+ Log.d(this, "Remote exception calling onAvailableCallEndpointsChanged");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onMuteStateChanged(boolean isMuted) {
+ if (!mInCallServices.isEmpty()) {
+ Log.i(this, "Calling onMuteStateChanged");
+ mInCallServices.values().forEach(inCallServices -> {
+ for (IInCallService inCallService : inCallServices.values()) {
+ try {
+ inCallService.onMuteStateChanged(isMuted);
+ } catch (RemoteException ignored) {
+ Log.d(this, "Remote exception calling onMuteStateChanged");
+ }
+ }
+ });
}
}
@@ -1334,19 +1598,22 @@ public class InCallController extends CallsManagerListenerBase implements
public void onCanAddCallChanged(boolean canAddCall) {
if (!mInCallServices.isEmpty()) {
Log.i(this, "onCanAddCallChanged : %b", canAddCall);
- for (IInCallService inCallService : mInCallServices.values()) {
- try {
- inCallService.onCanAddCallChanged(canAddCall);
- } catch (RemoteException ignored) {
+ mInCallServices.values().forEach(inCallServices -> {
+ for (IInCallService inCallService : inCallServices.values()) {
+ try {
+ inCallService.onCanAddCallChanged(canAddCall);
+ } catch (RemoteException ignored) {
+ }
}
- }
+ });
}
}
void onPostDialWait(Call call, String remaining) {
- if (!mInCallServices.isEmpty()) {
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
- for (IInCallService inCallService : mInCallServices.values()) {
+ for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
} catch (RemoteException ignored) {
@@ -1406,6 +1673,7 @@ public class InCallController extends CallsManagerListenerBase implements
}
if (shouldStart) {
+ // Note, not checking return value, as this op call is merely for tracing use
mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
mContext.getOpPackageName(), false, null, null);
mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.CAMERA);
@@ -1420,9 +1688,10 @@ public class InCallController extends CallsManagerListenerBase implements
}
}
- void bringToForeground(boolean showDialpad) {
- if (!mInCallServices.isEmpty()) {
- for (IInCallService inCallService : mInCallServices.values()) {
+ @VisibleForTesting
+ public void bringToForeground(boolean showDialpad, UserHandle callingUser) {
+ if (mInCallServices.containsKey(callingUser)) {
+ for (IInCallService inCallService : mInCallServices.get(callingUser).values()) {
try {
inCallService.bringToForeground(showDialpad);
} catch (RemoteException ignored) {
@@ -1433,20 +1702,33 @@ public class InCallController extends CallsManagerListenerBase implements
}
}
- void silenceRinger() {
- if (!mInCallServices.isEmpty()) {
- for (IInCallService inCallService : mInCallServices.values()) {
- try {
- inCallService.silenceRinger();
- } catch (RemoteException ignored) {
+ @VisibleForTesting
+ public Map<UserHandle, Map<InCallServiceInfo, IInCallService>> getInCallServices() {
+ return mInCallServices;
+ }
+
+ @VisibleForTesting
+ public Map<UserHandle, CarSwappingInCallServiceConnection> getInCallServiceConnections() {
+ return mInCallServiceConnections;
+ }
+
+ void silenceRinger(Set<UserHandle> userHandles) {
+ userHandles.forEach(userHandle -> {
+ if (mInCallServices.containsKey(userHandle)) {
+ for (IInCallService inCallService : mInCallServices.get(userHandle).values()) {
+ try {
+ inCallService.silenceRinger();
+ } catch (RemoteException ignored) {
+ }
}
}
- }
+ });
}
private void notifyConnectionEvent(Call call, String event, Bundle extras) {
- if (!mInCallServices.isEmpty()) {
- for (IInCallService inCallService : mInCallServices.values()) {
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
+ for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
Log.i(this, "notifyConnectionEvent {Call: %s, Event: %s, Extras:[%s]}",
(call != null ? call.toString() : "null"),
@@ -1460,9 +1742,11 @@ public class InCallController extends CallsManagerListenerBase implements
}
private void notifyRttInitiationFailure(Call call, int reason) {
- if (!mInCallServices.isEmpty()) {
- mInCallServices.entrySet().stream()
- .filter((entry) -> entry.getKey().equals(mInCallServiceConnection.getInfo()))
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
+ mInCallServices.get(userFromCall).entrySet().stream()
+ .filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
+ get(userFromCall).getInfo()))
.forEach((entry) -> {
try {
Log.i(this, "notifyRttFailure, call %s, incall %s",
@@ -1476,9 +1760,11 @@ public class InCallController extends CallsManagerListenerBase implements
}
private void notifyRemoteRttRequest(Call call, int requestId) {
- if (!mInCallServices.isEmpty()) {
- mInCallServices.entrySet().stream()
- .filter((entry) -> entry.getKey().equals(mInCallServiceConnection.getInfo()))
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
+ mInCallServices.get(userFromCall).entrySet().stream()
+ .filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
+ get(userFromCall).getInfo()))
.forEach((entry) -> {
try {
Log.i(this, "notifyRemoteRttRequest, call %s, incall %s",
@@ -1492,8 +1778,9 @@ public class InCallController extends CallsManagerListenerBase implements
}
private void notifyHandoverFailed(Call call, int error) {
- if (!mInCallServices.isEmpty()) {
- for (IInCallService inCallService : mInCallServices.values()) {
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
+ for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
inCallService.onHandoverFailed(mCallIdMapper.getCallId(call), error);
} catch (RemoteException ignored) {
@@ -1503,8 +1790,9 @@ public class InCallController extends CallsManagerListenerBase implements
}
private void notifyHandoverComplete(Call call) {
- if (!mInCallServices.isEmpty()) {
- for (IInCallService inCallService : mInCallServices.values()) {
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
+ for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
inCallService.onHandoverComplete(mCallIdMapper.getCallId(call));
} catch (RemoteException ignored) {
@@ -1516,22 +1804,22 @@ public class InCallController extends CallsManagerListenerBase implements
/**
* Unbinds an existing bound connection to the in-call app.
*/
- public void unbindFromServices() {
+ public void unbindFromServices(UserHandle userHandle) {
try {
mContext.unregisterReceiver(mPackageChangedReceiver);
} catch (IllegalArgumentException e) {
// Ignore this -- we may or may not have registered it, but when we bind, we want to
// unregister no matter what.
}
- if (mInCallServiceConnection != null) {
- mInCallServiceConnection.disconnect();
- mInCallServiceConnection = null;
+ if (mInCallServiceConnections.containsKey(userHandle)) {
+ mInCallServiceConnections.get(userHandle).disconnect();
+ mInCallServiceConnections.remove(userHandle);
}
- if (mNonUIInCallServiceConnections != null) {
- mNonUIInCallServiceConnections.disconnect();
- mNonUIInCallServiceConnections = null;
+ if (mNonUIInCallServiceConnections.containsKey(userHandle)) {
+ mNonUIInCallServiceConnections.get(userHandle).disconnect();
+ mNonUIInCallServiceConnections.remove(userHandle);
}
- mInCallServices.clear();
+ mInCallServices.remove(userHandle);
}
/**
@@ -1543,9 +1831,18 @@ public class InCallController extends CallsManagerListenerBase implements
*/
@VisibleForTesting
public void bindToServices(Call call) {
- if (mInCallServiceConnection == null) {
+ UserHandle userFromCall = getUserFromCall(call);
+ UserHandle parentUser = null;
+ UserManager um = mContext.getSystemService(UserManager.class);
+
+ if (um.isManagedProfile(userFromCall.getIdentifier())) {
+ parentUser = um.getProfileParent(userFromCall);
+ Log.i(this, "child:%s parent:%s", userFromCall, parentUser);
+ }
+
+ if (!mInCallServiceConnections.containsKey(userFromCall)) {
InCallServiceConnection dialerInCall = null;
- InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent();
+ InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent(userFromCall);
Log.i(this, "defaultDialer: " + defaultDialerComponentInfo);
if (defaultDialerComponentInfo != null &&
!defaultDialerComponentInfo.getComponentName().equals(
@@ -1554,28 +1851,44 @@ public class InCallController extends CallsManagerListenerBase implements
}
Log.i(this, "defaultDialer: " + dialerInCall);
- InCallServiceInfo systemInCallInfo = getInCallServiceComponent(
+ InCallServiceInfo systemInCallInfo = getInCallServiceComponent(userFromCall,
mDefaultDialerCache.getSystemDialerComponent(), IN_CALL_SERVICE_TYPE_SYSTEM_UI);
EmergencyInCallServiceConnection systemInCall =
new EmergencyInCallServiceConnection(systemInCallInfo, dialerInCall);
systemInCall.setHasEmergency(mCallsManager.isInEmergencyCall());
InCallServiceConnection carModeInCall = null;
- InCallServiceInfo carModeComponentInfo = getCurrentCarModeComponent();
+ InCallServiceInfo carModeComponentInfo = getCurrentCarModeComponent(userFromCall);
+ InCallServiceInfo carModeComponentInfoForParentUser = null;
+ if(parentUser != null) {
+ //query using parent user too
+ carModeComponentInfoForParentUser = getCurrentCarModeComponent(
+ parentUser);
+ }
+
if (carModeComponentInfo != null &&
!carModeComponentInfo.getComponentName().equals(
mDefaultDialerCache.getSystemDialerComponent())) {
carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
+ } else if (carModeComponentInfo == null &&
+ carModeComponentInfoForParentUser != null &&
+ !carModeComponentInfoForParentUser.getComponentName().equals(
+ mDefaultDialerCache.getSystemDialerComponent())) {
+ carModeInCall = new InCallServiceBindingConnection(
+ carModeComponentInfoForParentUser, parentUser);
+ Log.i(this, "Using car mode component queried using parent handle");
}
- mInCallServiceConnection =
- new CarSwappingInCallServiceConnection(systemInCall, carModeInCall);
+ mInCallServiceConnections.put(userFromCall,
+ new CarSwappingInCallServiceConnection(systemInCall, carModeInCall));
}
- mInCallServiceConnection.chooseInitialInCallService(shouldUseCarModeUI());
+ CarSwappingInCallServiceConnection inCallServiceConnection =
+ mInCallServiceConnections.get(userFromCall);
+ inCallServiceConnection.chooseInitialInCallService(shouldUseCarModeUI());
// Actually try binding to the UI InCallService.
- if (mInCallServiceConnection.connect(call) ==
+ if (inCallServiceConnection.connect(call) ==
InCallServiceConnection.CONNECTION_SUCCEEDED || 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,
@@ -1591,41 +1904,75 @@ public class InCallController extends CallsManagerListenerBase implements
IntentFilter packageChangedFilter = new IntentFilter(Intent.ACTION_PACKAGE_CHANGED);
packageChangedFilter.addDataScheme("package");
- mContext.registerReceiver(mPackageChangedReceiver, packageChangedFilter);
+ mContext.registerReceiverAsUser(mPackageChangedReceiver, UserHandle.ALL,
+ packageChangedFilter, null, null);
}
- private void updateNonUiInCallServices() {
+ private void updateNonUiInCallServices(Call call) {
+ UserHandle userFromCall = getUserFromCall(call);
+ UserHandle parentUser = null;
+
+ UserManager um = mContext.getSystemService(UserManager.class);
+ if(um.isManagedProfile(userFromCall.getIdentifier()))
+ {
+ parentUser = um.getProfileParent(userFromCall);
+ }
+
List<InCallServiceInfo> nonUIInCallComponents =
- getInCallServiceComponents(IN_CALL_SERVICE_TYPE_NON_UI);
+ getInCallServiceComponents(userFromCall, IN_CALL_SERVICE_TYPE_NON_UI);
+ List<InCallServiceInfo> nonUIInCallComponentsForParent = new ArrayList<>();
+ if(parentUser != null)
+ {
+ //also get Non-UI services using parent handle.
+ nonUIInCallComponentsForParent =
+ getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_NON_UI);
+
+ }
List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
for (InCallServiceInfo serviceInfo : nonUIInCallComponents) {
nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo));
}
+
+ //add nonUI InCall services queried using parent user (if any)
+ for (InCallServiceInfo serviceInfo : nonUIInCallComponentsForParent) {
+ if (nonUIInCallComponents.contains(serviceInfo)) {
+ //skip dups
+ Log.i(this, "skipped duplicate component found using parent user: "
+ + serviceInfo.getComponentName());
+ } else {
+ nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo, parentUser));
+ Log.i(this,
+ "added component queried using parent user: "
+ + serviceInfo.getComponentName());
+ }
+ }
+
List<String> callCompanionApps = mCallsManager
.getRoleManagerAdapter().getCallCompanionApps();
if (callCompanionApps != null && !callCompanionApps.isEmpty()) {
for (String pkg : callCompanionApps) {
- InCallServiceInfo info = getInCallServiceComponent(pkg,
+ InCallServiceInfo info = getInCallServiceComponent(userFromCall, pkg,
IN_CALL_SERVICE_TYPE_COMPANION, true /* ignoreDisabled */);
if (info != null) {
nonUIInCalls.add(new InCallServiceBindingConnection(info));
}
}
}
- mNonUIInCallServiceConnections = new NonUIInCallServiceConnectionCollection(
- nonUIInCalls);
+ mNonUIInCallServiceConnections.put(userFromCall, new NonUIInCallServiceConnectionCollection(
+ nonUIInCalls));
}
private void connectToNonUiInCallServices(Call call) {
- if (mNonUIInCallServiceConnections == null) {
- updateNonUiInCallServices();
+ UserHandle userFromCall = getUserFromCall(call);
+ if (!mNonUIInCallServiceConnections.containsKey(userFromCall)) {
+ updateNonUiInCallServices(call);
}
- mNonUIInCallServiceConnections.connect(call);
+ mNonUIInCallServiceConnections.get(userFromCall).connect(call);
}
- private @Nullable InCallServiceInfo getDefaultDialerComponent() {
+ private @Nullable InCallServiceInfo getDefaultDialerComponent(UserHandle userHandle) {
String defaultPhoneAppName = mDefaultDialerCache.getDefaultDialerApplication(
- mCallsManager.getCurrentUserHandle().getIdentifier());
+ userHandle.getIdentifier());
String systemPhoneAppName = mDefaultDialerCache.getSystemDialerApplication();
Log.d(this, "getDefaultDialerComponent: defaultPhoneAppName=[%s]", defaultPhoneAppName);
@@ -1635,10 +1982,10 @@ public class InCallController extends CallsManagerListenerBase implements
InCallServiceInfo defaultPhoneAppComponent =
(systemPhoneAppName != null && systemPhoneAppName.equals(defaultPhoneAppName)) ?
/* The defaultPhoneApp is also the systemPhoneApp. Get systemPhoneApp info*/
- getInCallServiceComponent(defaultPhoneAppName,
+ getInCallServiceComponent(userHandle, defaultPhoneAppName,
IN_CALL_SERVICE_TYPE_SYSTEM_UI, true /* ignoreDisabled */)
/* The defaultPhoneApp is NOT the systemPhoneApp. Get defaultPhoneApp info*/
- : getInCallServiceComponent(defaultPhoneAppName,
+ : getInCallServiceComponent(userHandle, defaultPhoneAppName,
IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI, true /* ignoreDisabled */);
Log.d(this, "getDefaultDialerComponent: defaultPhoneAppComponent=[%s]",
@@ -1650,55 +1997,89 @@ public class InCallController extends CallsManagerListenerBase implements
return defaultPhoneAppComponent;
}
- private InCallServiceInfo getCurrentCarModeComponent() {
- return getInCallServiceComponent(mCarModeTracker.getCurrentCarModePackage(),
+ private InCallServiceInfo getCurrentCarModeComponent(UserHandle userHandle) {
+ return getInCallServiceComponent(userHandle,
+ mCarModeTracker.getCurrentCarModePackage(),
IN_CALL_SERVICE_TYPE_CAR_MODE_UI, true /* ignoreDisabled */);
}
- private InCallServiceInfo getInCallServiceComponent(ComponentName componentName, int type) {
- List<InCallServiceInfo> list = getInCallServiceComponents(componentName, type);
+ private InCallServiceInfo getInCallServiceComponent(UserHandle userHandle,
+ ComponentName componentName, int type) {
+ List<InCallServiceInfo> list = getInCallServiceComponents(userHandle,
+ componentName, type);
if (list != null && !list.isEmpty()) {
return list.get(0);
} else {
// Last Resort: Try to bind to the ComponentName given directly.
Log.e(this, new Exception(), "Package Manager could not find ComponentName: "
+ componentName + ". Trying to bind anyway.");
- return new InCallServiceInfo(componentName, false, false, type);
+ return new InCallServiceInfo(componentName, false, false, type, false);
}
}
- private InCallServiceInfo getInCallServiceComponent(String packageName, int type,
- boolean ignoreDisabled) {
- List<InCallServiceInfo> list = getInCallServiceComponents(packageName, type,
- ignoreDisabled);
+ private InCallServiceInfo getInCallServiceComponent(UserHandle userHandle,
+ String packageName, int type, boolean ignoreDisabled) {
+ List<InCallServiceInfo> list = getInCallServiceComponents(userHandle,
+ packageName, type, ignoreDisabled);
if (list != null && !list.isEmpty()) {
return list.get(0);
}
return null;
}
- private List<InCallServiceInfo> getInCallServiceComponents(int type) {
- return getInCallServiceComponents(null, null, type);
+ private List<InCallServiceInfo> getInCallServiceComponents(
+ UserHandle userHandle, int type) {
+ return getInCallServiceComponents(userHandle, null, null, type);
}
- private List<InCallServiceInfo> getInCallServiceComponents(String packageName, int type,
- boolean ignoreDisabled) {
- return getInCallServiceComponents(packageName, null, type, ignoreDisabled);
+ private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
+ String packageName, int type, boolean ignoreDisabled) {
+ return getInCallServiceComponents(userHandle, packageName, null,
+ type, ignoreDisabled);
}
- private List<InCallServiceInfo> getInCallServiceComponents(ComponentName componentName,
- int type) {
- return getInCallServiceComponents(null, componentName, type);
+ private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
+ ComponentName componentName, int type) {
+ return getInCallServiceComponents(userHandle, null, componentName, type);
}
- private List<InCallServiceInfo> getInCallServiceComponents(String packageName,
- ComponentName componentName, int requestedType) {
- return getInCallServiceComponents(packageName, componentName, requestedType,
- true /* ignoreDisabled */);
+ private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
+ String packageName, ComponentName componentName, int requestedType) {
+ return getInCallServiceComponents(userHandle, packageName,
+ componentName, requestedType, true /* ignoreDisabled */);
}
+ private boolean canInteractAcrossUsersOrProfiles(ServiceInfo serviceInfo,
+ PackageManager packageManager) {
+ String op = AppOpsManager.permissionToOp("android.permission.INTERACT_ACROSS_PROFILES");
+ String[] uidPackages = packageManager.getPackagesForUid(serviceInfo.applicationInfo.uid);
- private List<InCallServiceInfo> getInCallServiceComponents(String packageName,
- ComponentName componentName, int requestedType, boolean ignoreDisabled) {
+ boolean hasInteractAcrossProfiles = Arrays.stream(uidPackages).anyMatch(
+ p -> ((packageManager.checkPermission(
+ Manifest.permission.INTERACT_ACROSS_PROFILES,
+ p) == PackageManager.PERMISSION_GRANTED)
+ ));
+ boolean hasInteractAcrossUsers = Arrays.stream(uidPackages).anyMatch(
+ p -> ((packageManager.checkPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS,
+ p) == PackageManager.PERMISSION_GRANTED)
+ ));
+ boolean hasInteractAcrossProfilesAppOp = Arrays.stream(uidPackages).anyMatch(
+ p -> (AppOpsManager.MODE_ALLOWED == mAppOpsManager.checkOpNoThrow(
+ op, serviceInfo.applicationInfo.uid, p))
+ );
+ Log.i(this,
+ "packageName:%s INTERACT_ACROSS_USERS:%b INTERACT_ACROSS_PROFILES:%b "
+ + "INTERACT_ACROSS_PROFILES_APPOP:%b",
+ uidPackages[0], hasInteractAcrossUsers, hasInteractAcrossProfiles,
+ hasInteractAcrossProfilesAppOp);
+
+ return (hasInteractAcrossUsers || hasInteractAcrossProfiles
+ || hasInteractAcrossProfilesAppOp);
+ }
+
+ private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
+ String packageName, ComponentName componentName,
+ int requestedType, boolean ignoreDisabled) {
List<InCallServiceInfo> retval = new LinkedList<>();
Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
@@ -1708,16 +2089,20 @@ public class InCallController extends CallsManagerListenerBase implements
if (componentName != null) {
serviceIntent.setComponent(componentName);
}
-
+ Log.i(this,
+ "getComponents, pkgname: " + packageName + " comp: " + componentName + " userid: "
+ + userHandle.getIdentifier() + " requestedType: " + requestedType);
PackageManager packageManager = mContext.getPackageManager();
- Context userContext = mContext.createContextAsUser(mCallsManager.getCurrentUserHandle(),
+ Context userContext = mContext.createContextAsUser(userHandle,
0 /* flags */);
PackageManager userPackageManager = userContext != null ?
userContext.getPackageManager() : packageManager;
+
+
for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS,
- mCallsManager.getCurrentUserHandle().getIdentifier())) {
+ userHandle.getIdentifier())) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
@@ -1728,8 +2113,12 @@ public class InCallController extends CallsManagerListenerBase implements
serviceInfo.metaData.getBoolean(
TelecomManager.METADATA_INCLUDE_SELF_MANAGED_CALLS, false);
- int currentType = getInCallServiceType(entry.serviceInfo, packageManager,
- packageName);
+ int currentType = getInCallServiceType(userHandle,
+ entry.serviceInfo, packageManager, packageName);
+
+ boolean hasInteractAcrossUserOrProfilePerm = canInteractAcrossUsersOrProfiles(
+ entry.serviceInfo, packageManager);
+
ComponentName foundComponentName =
new ComponentName(serviceInfo.packageName, serviceInfo.name);
if (currentType == IN_CALL_SERVICE_TYPE_NON_UI) {
@@ -1745,9 +2134,16 @@ public class InCallController extends CallsManagerListenerBase implements
isRequestedType = requestedType == currentType;
}
+ Log.i(this,
+ "found:%s isRequestedtype:%b isEnabled:%b ignoreDisabled:%b "
+ + "hasCrossProfilePerm:%b",
+ foundComponentName, isRequestedType, isEnabled, ignoreDisabled,
+ hasInteractAcrossUserOrProfilePerm);
+
if ((!ignoreDisabled || isEnabled) && isRequestedType) {
retval.add(new InCallServiceInfo(foundComponentName, isExternalCallsSupported,
- isSelfManageCallsSupported, requestedType));
+ isSelfManageCallsSupported, requestedType,
+ hasInteractAcrossUserOrProfilePerm));
}
}
}
@@ -1780,8 +2176,8 @@ public class InCallController extends CallsManagerListenerBase implements
/**
* Returns the type of InCallService described by the specified serviceInfo.
*/
- private int getInCallServiceType(ServiceInfo serviceInfo, PackageManager packageManager,
- String packageName) {
+ private int getInCallServiceType(UserHandle userHandle, ServiceInfo serviceInfo,
+ PackageManager packageManager, String packageName) {
// Verify that the InCallService requires the BIND_INCALL_SERVICE permission which
// enforces that only Telecom can bind to it.
boolean hasServiceBindPermission = serviceInfo.permission != null &&
@@ -1833,7 +2229,7 @@ public class InCallController extends CallsManagerListenerBase implements
// Check to see that it is the default dialer package
boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
mDefaultDialerCache.getDefaultDialerApplication(
- mCallsManager.getCurrentUserHandle().getIdentifier()));
+ userHandle.getIdentifier()));
if (isDefaultDialerPackage && isUIService) {
return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
}
@@ -1852,11 +2248,11 @@ public class InCallController extends CallsManagerListenerBase implements
return IN_CALL_SERVICE_TYPE_INVALID;
}
- private void adjustServiceBindingsForEmergency() {
+ private void adjustServiceBindingsForEmergency(UserHandle userHandle) {
// The connected UI is not the system UI, so lets check if we should switch them
// if there exists an emergency number.
if (mCallsManager.isInEmergencyCall()) {
- mInCallServiceConnection.setHasEmergency(true);
+ mInCallServiceConnections.get(userHandle).setHasEmergency(true);
}
}
@@ -1869,7 +2265,7 @@ public class InCallController extends CallsManagerListenerBase implements
* @param service The {@link IInCallService} implementation.
* @return True if we successfully connected.
*/
- private boolean onConnected(InCallServiceInfo info, IBinder service) {
+ private boolean onConnected(InCallServiceInfo info, IBinder service, UserHandle userHandle) {
Log.i(this, "onConnected to %s", info.getComponentName());
if (info.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
@@ -1878,8 +2274,9 @@ public class InCallController extends CallsManagerListenerBase implements
trackCallingUserInterfaceStarted(info);
}
IInCallService inCallService = IInCallService.Stub.asInterface(service);
- mInCallServices.put(info, inCallService);
-
+ mInCallServices.putIfAbsent(userHandle,
+ new ArrayMap<InCallController.InCallServiceInfo, IInCallService>());
+ mInCallServices.get(userHandle).put(info, inCallService);
try {
inCallService.setInCallAdapter(
new InCallAdapter(
@@ -1889,6 +2286,8 @@ public class InCallController extends CallsManagerListenerBase implements
info.getComponentName().getPackageName()));
} catch (RemoteException e) {
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;
}
@@ -1924,10 +2323,11 @@ public class InCallController extends CallsManagerListenerBase implements
return 0;
}
+ UserHandle userFromCall = getUserFromCall(call);
// Only send the RTT call if it's a UI in-call service
boolean includeRttCall = false;
- if (mInCallServiceConnection != null) {
- includeRttCall = info.equals(mInCallServiceConnection.getInfo());
+ if (mInCallServiceConnections.containsKey(userFromCall)) {
+ includeRttCall = info.equals(mInCallServiceConnections.get(userFromCall).getInfo());
}
// Track the call if we don't already know about it.
@@ -1953,14 +2353,16 @@ public class InCallController extends CallsManagerListenerBase implements
*
* @param disconnectedInfo The {@link InCallServiceInfo} of the service which disconnected.
*/
- private void onDisconnected(InCallServiceInfo disconnectedInfo) {
+ private void onDisconnected(InCallServiceInfo disconnectedInfo, UserHandle userHandle) {
Log.i(this, "onDisconnected from %s", disconnectedInfo.getComponentName());
if (disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
|| disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
|| disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI) {
trackCallingUserInterfaceStopped(disconnectedInfo);
}
- mInCallServices.remove(disconnectedInfo);
+ if (mInCallServices.containsKey(userHandle)) {
+ mInCallServices.get(userHandle).remove(disconnectedInfo);
+ }
}
/**
@@ -1969,24 +2371,41 @@ public class InCallController extends CallsManagerListenerBase implements
* @param call The {@link Call}.
*/
private void updateCall(Call call) {
- updateCall(call, false /* videoProviderChanged */, false);
+ updateCall(call, false /* videoProviderChanged */, false, null);
}
/**
* Informs all {@link InCallService} instances of the updated call information.
*
- * @param call The {@link Call}.
+ * @param call The {@link Call}.
* @param videoProviderChanged {@code true} if the video provider changed, {@code false}
- * otherwise.
- * @param rttInfoChanged {@code true} if any information about the RTT session changed,
- * {@code false} otherwise.
+ * otherwise.
+ * @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
+ * 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) {
- if (!mInCallServices.isEmpty()) {
+ private void updateCall(Call call, boolean videoProviderChanged, boolean rttInfoChanged,
+ String exceptPackageName) {
+ UserHandle userFromCall = getUserFromCall(call);
+ if (mInCallServices.containsKey(userFromCall)) {
Log.i(this, "Sending updateCall %s", call);
List<ComponentName> componentsUpdated = new ArrayList<>();
- for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+ for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+ get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
+ ComponentName componentName = info.getComponentName();
+
+ // If specified, skip ICS if it matches the package name. Used for cases where on
+ // ICS makes an update to extras and we want to skip updating the same ICS with the
+ // change that it implemented.
+ if (exceptPackageName != null
+ && componentName.getPackageName().equals(exceptPackageName)) {
+ continue;
+ }
+
if (call.isExternalCall() && !info.isExternalCallsSupported()) {
continue;
}
@@ -2001,10 +2420,10 @@ public class InCallController extends CallsManagerListenerBase implements
videoProviderChanged /* includeVideoProvider */,
mCallsManager.getPhoneAccountRegistrar(),
info.isExternalCallsSupported(),
- rttInfoChanged && info.equals(mInCallServiceConnection.getInfo()),
+ rttInfoChanged && info.equals(
+ mInCallServiceConnections.get(userFromCall).getInfo()),
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
- ComponentName componentName = info.getComponentName();
IInCallService inCallService = entry.getValue();
componentsUpdated.add(componentName);
@@ -2022,7 +2441,8 @@ public class InCallController extends CallsManagerListenerBase implements
* Adds the call to the list of calls tracked by the {@link InCallController}.
* @param call The call to add.
*/
- private void addCall(Call call) {
+ @VisibleForTesting
+ public void addCall(Call call) {
if (mCallIdMapper.getCalls().size() == 0) {
mAppOpsManager.startWatchingActive(new String[] { OPSTR_RECORD_AUDIO },
java.lang.Runnable::run, this);
@@ -2041,8 +2461,11 @@ public class InCallController extends CallsManagerListenerBase implements
/**
* @return true if we are bound to the UI InCallService and it is connected.
*/
- private boolean isBoundAndConnectedToServices() {
- return mInCallServiceConnection != null && mInCallServiceConnection.isConnected();
+ private boolean isBoundAndConnectedToServices(UserHandle userHandle) {
+ if (!mInCallServiceConnections.containsKey(userHandle)) {
+ return false;
+ }
+ return mInCallServiceConnections.get(userHandle).isConnected();
}
/**
@@ -2061,15 +2484,17 @@ public class InCallController extends CallsManagerListenerBase implements
public void dump(IndentingPrintWriter pw) {
pw.println("mInCallServices (InCalls registered):");
pw.increaseIndent();
- for (InCallServiceInfo info : mInCallServices.keySet()) {
- pw.println(info);
- }
+ mInCallServices.values().forEach(inCallServices -> {
+ for (InCallServiceInfo info : inCallServices.keySet()) {
+ pw.println(info);
+ }
+ });
pw.decreaseIndent();
pw.println("ServiceConnections (InCalls bound):");
pw.increaseIndent();
- if (mInCallServiceConnection != null) {
- mInCallServiceConnection.dump(pw);
+ for (InCallServiceConnection inCallServiceConnection : mInCallServiceConnections.values()) {
+ inCallServiceConnection.dump(pw);
}
pw.decreaseIndent();
@@ -2079,22 +2504,25 @@ public class InCallController extends CallsManagerListenerBase implements
/**
* @return The package name of the UI which is currently bound, or null if none.
*/
- private ComponentName getConnectedUi() {
- InCallServiceInfo connectedUi = mInCallServices.keySet().stream().filter(
- i -> i.getType() == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI
- || i.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI)
- .findAny()
- .orElse(null);
- if (connectedUi != null) {
- return connectedUi.mComponentName;
+ private ComponentName getConnectedUi(UserHandle userHandle) {
+ if (mInCallServices.containsKey(userHandle)) {
+ InCallServiceInfo connectedUi = mInCallServices.get(
+ userHandle).keySet().stream().filter(
+ i -> i.getType() == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI
+ || i.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI)
+ .findAny()
+ .orElse(null);
+ if (connectedUi != null) {
+ return connectedUi.mComponentName;
+ }
}
return null;
}
- public boolean doesConnectedDialerSupportRinging() {
+ public boolean doesConnectedDialerSupportRinging(UserHandle userHandle) {
String ringingPackage = null;
- ComponentName connectedPackage = getConnectedUi();
+ ComponentName connectedPackage = getConnectedUi(userHandle);
if (connectedPackage != null) {
ringingPackage = connectedPackage.getPackageName().trim();
Log.d(this, "doesConnectedDialerSupportRinging: alreadyConnectedPackage=%s",
@@ -2104,7 +2532,7 @@ public class InCallController extends CallsManagerListenerBase implements
if (TextUtils.isEmpty(ringingPackage)) {
// The current in-call UI returned nothing, so lets use the default dialer.
ringingPackage = mDefaultDialerCache.getRoleManagerAdapter().getDefaultDialerApp(
- mCallsManager.getCurrentUserHandle().getIdentifier());
+ userHandle.getIdentifier());
if (ringingPackage != null) {
Log.d(this, "doesConnectedDialerSupportRinging: notCurentlyConnectedPackage=%s",
ringingPackage);
@@ -2119,7 +2547,7 @@ public class InCallController extends CallsManagerListenerBase implements
.setPackage(ringingPackage);
List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServicesAsUser(
intent, PackageManager.GET_META_DATA,
- mCallsManager.getCurrentUserHandle().getIdentifier());
+ userHandle.getIdentifier());
if (entries.isEmpty()) {
Log.w(this, "doesConnectedDialerSupportRinging: couldn't find dialer's package info"
+ " <sad trombone>");
@@ -2151,15 +2579,32 @@ public class InCallController extends CallsManagerListenerBase implements
return childCalls;
}
- private ParcelableCall sanitizeParcelableCallForService(
+ @VisibleForTesting
+ public ParcelableCall sanitizeParcelableCallForService(
InCallServiceInfo info, ParcelableCall parcelableCall) {
ParcelableCall.ParcelableCallBuilder builder =
ParcelableCall.ParcelableCallBuilder.fromParcelableCall(parcelableCall);
- // Check for contacts permission. If it's not there, remove the contactsDisplayName.
PackageManager pm = mContext.getPackageManager();
+
+ // Check for contacts permission.
if (pm.checkPermission(Manifest.permission.READ_CONTACTS,
info.getComponentName().getPackageName()) != PackageManager.PERMISSION_GRANTED) {
+ // contacts permission is not present...
+
+ // removing the contactsDisplayName
builder.setContactDisplayName(null);
+ builder.setContactPhotoUri(null);
+
+ // removing the Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB extra
+ if (parcelableCall.getExtras() != null) {
+ Bundle callBundle = parcelableCall.getExtras();
+ if (callBundle.containsKey(
+ android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB)) {
+ Bundle newBundle = callBundle.deepCopy();
+ newBundle.remove(android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB);
+ builder.setExtras(newBundle);
+ }
+ }
}
// TODO: move all the other service-specific sanitizations in here
@@ -2181,8 +2626,8 @@ public class InCallController extends CallsManagerListenerBase implements
// Disabled InCallService should also be considered as a valid InCallService here so that
// it can be added to the CarModeTracker, in case it will be enabled in future.
InCallServiceInfo info =
- getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_CAR_MODE_UI,
- false /* ignoreDisabled */);
+ getInCallServiceComponent(mCallsManager.getCurrentUserHandle(),
+ packageName, IN_CALL_SERVICE_TYPE_CAR_MODE_UI, false /* ignoreDisabled */);
return info != null && info.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
}
@@ -2231,17 +2676,49 @@ public class InCallController extends CallsManagerListenerBase implements
public void updateCarModeForConnections() {
Log.i(this, "updateCarModeForConnections: car mode apps: %s",
mCarModeTracker.getCarModeApps().stream().collect(Collectors.joining(", ")));
- if (mInCallServiceConnection != null) {
- if (shouldUseCarModeUI()) {
- Log.i(this, "updateCarModeForConnections: potentially update car mode app.");
- mInCallServiceConnection.changeCarModeApp(
- mCarModeTracker.getCurrentCarModePackage());
- } else {
- if (mInCallServiceConnection.isCarMode()) {
- Log.i(this, "updateCarModeForConnections: car mode no longer "
- + "applicable; disabling");
- mInCallServiceConnection.disableCarMode();
- }
+
+ UserManager um = mContext.getSystemService(UserManager.class);
+ UserHandle currentUser = mCallsManager.getCurrentUserHandle();
+ UserHandle childUser = findChildManagedProfileUser(currentUser, um);
+
+ CarSwappingInCallServiceConnection inCallServiceConnectionForCurrentUser = null;
+ CarSwappingInCallServiceConnection inCallServiceConnectionForChildUser = null;
+
+ Log.i(this, "update carmode current:%s parent:%s", currentUser, childUser);
+ if (mInCallServiceConnections.containsKey(currentUser)) {
+ inCallServiceConnectionForCurrentUser = mInCallServiceConnections.
+ get(currentUser);
+ }
+ if (childUser != null && mInCallServiceConnections.containsKey(childUser)) {
+ inCallServiceConnectionForChildUser = mInCallServiceConnections.
+ get(childUser);
+ }
+
+ if (shouldUseCarModeUI()) {
+ Log.i(this, "updateCarModeForConnections: potentially update car mode app.");
+ //always pass current user to changeCarMode. That will ultimately be used for bindAsUser
+ if (inCallServiceConnectionForCurrentUser != null) {
+ inCallServiceConnectionForCurrentUser.changeCarModeApp(
+ mCarModeTracker.getCurrentCarModePackage(),
+ currentUser);
+ }
+ if (inCallServiceConnectionForChildUser != null) {
+ inCallServiceConnectionForChildUser.changeCarModeApp(
+ mCarModeTracker.getCurrentCarModePackage(),
+ currentUser);
+ }
+ } else {
+ if (inCallServiceConnectionForCurrentUser != null
+ && inCallServiceConnectionForCurrentUser.isCarMode()) {
+ Log.i(this, "updateCarModeForConnections: car mode no longer "
+ + "applicable for current user; disabling");
+ inCallServiceConnectionForCurrentUser.disableCarMode();
+ }
+ if (inCallServiceConnectionForChildUser != null
+ && inCallServiceConnectionForChildUser.isCarMode()) {
+ Log.i(this, "updateCarModeForConnections: car mode no longer "
+ + "applicable for child user; disabling");
+ inCallServiceConnectionForChildUser.disableCarMode();
}
}
}
@@ -2303,6 +2780,7 @@ public class InCallController extends CallsManagerListenerBase implements
&& !isCarrierPrivilegedUsingMicDuringVoipCall();
if (wasUsingMicrophone != mIsCallUsingMicrophone) {
if (mIsCallUsingMicrophone) {
+ // Note, not checking return value, as this op call is merely for tracing use
mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
mContext.getOpPackageName(), false, null, null);
mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.MICROPHONE);
@@ -2348,7 +2826,7 @@ public class InCallController extends CallsManagerListenerBase implements
== PermissionChecker.PERMISSION_GRANTED;
}
- private void sendCrashedInCallServiceNotification(String packageName) {
+ private void sendCrashedInCallServiceNotification(String packageName, UserHandle userHandle) {
PackageManager packageManager = mContext.getPackageManager();
CharSequence appName;
String systemDialer = mDefaultDialerCache.getSystemDialerApplication();
@@ -2376,8 +2854,8 @@ public class InCallController extends CallsManagerListenerBase implements
.setStyle(new Notification.BigTextStyle()
.bigText(mContext.getText(
R.string.notification_incallservice_not_responding_body)));
- notificationManager.notify(NOTIFICATION_TAG, IN_CALL_SERVICE_NOTIFICATION_ID,
- builder.build());
+ notificationManager.notifyAsUser(NOTIFICATION_TAG, IN_CALL_SERVICE_NOTIFICATION_ID,
+ builder.build(), userHandle);
}
private void updateCallTracking(Call call, InCallServiceInfo info, boolean isAdd) {
@@ -2386,4 +2864,25 @@ public class InCallController extends CallsManagerListenerBase implements
|| type == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
call.maybeOnInCallServiceTrackingChanged(isAdd, hasUi);
}
+
+ private UserHandle getUserFromCall(Call call) {
+ // Call may never be specified, so we can fall back to using the CallManager current user.
+ if (call == null) {
+ 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.
+ 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()))) {
+ return mCallsManager.getCurrentUserHandle();
+ }
+ return userFromCall;
+ }
+ }
}
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index 0367ba0cb..3cc4aacc6 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -170,7 +170,7 @@ public class InCallTonePlayer extends Thread {
private static final int RELATIVE_VOLUME_EMERGENCY = 100;
private static final int RELATIVE_VOLUME_HIPRI = 80;
- private static final int RELATIVE_VOLUME_LOPRI = 50;
+ private static final int RELATIVE_VOLUME_LOPRI = 30;
private static final int RELATIVE_VOLUME_UNDEFINED = -1;
// Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
@@ -470,12 +470,6 @@ public class InCallTonePlayer extends Thread {
@VisibleForTesting
public boolean startTone() {
- // Skip playing the end call tone if the volume is silenced.
- if (mToneId == TONE_CALL_ENDED && !mAudioManagerAdapter.isVolumeOverZero()) {
- Log.i(this, "startTone: skip end-call tone as device is silenced.");
- return false;
- }
-
// Tone already done; don't allow re-used
if (mState == STATE_STOPPED) {
return false;
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 12e780fc0..0d6acd51d 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -88,6 +88,7 @@ public class LogUtils {
public static final String CSW_REMOVE_CALL = "CSW.rC";
public static final String CSW_SET_IS_CONFERENCED = "CSW.sIC";
public static final String CSW_ADD_CONFERENCE_CALL = "CSW.aCC";
+ public static final String CSA_SET_STATE = "CSA.sSS";
}
public final static class Events {
@@ -104,10 +105,17 @@ public class LogUtils {
public static final String SET_RINGING = "SET_RINGING";
public static final String SET_ANSWERED = "SET_ANSWERED";
public static final String SET_DISCONNECTED = "SET_DISCONNECTED";
+ public static final String SKIP_CALL_LOG = "SKIP_CALL_LOG";
+ public static final String LOG_CALL = "LOG_CALL";
public static final String SET_DISCONNECTING = "SET_DISCONNECTING";
public static final String SET_SELECT_PHONE_ACCOUNT = "SET_SELECT_PHONE_ACCOUNT";
public static final String SET_AUDIO_PROCESSING = "SET_AUDIO_PROCESSING";
public static final String SET_SIMULATED_RINGING = "SET_SIMULATED_RINGING";
+ public static final String REQUEST_RTT = "REQUEST_RTT";
+ public static final String RESPOND_TO_RTT_REQUEST = "RESPOND_TO_RTT_REQUEST";
+ public static final String SET_RRT_MODE = "SET_RTT_MODE";
+ public static final String ON_RTT_FAILED = "ON_RTT_FAILED";
+ public static final String ON_RTT_REQUEST = "ON_RTT_REQUEST";
public static final String REQUEST_HOLD = "REQUEST_HOLD";
public static final String REQUEST_UNHOLD = "REQUEST_UNHOLD";
public static final String REQUEST_DISCONNECT = "REQUEST_DISCONNECT";
@@ -168,6 +176,8 @@ public class LogUtils {
public static final String DIRECT_TO_VM_INITIATED = "DIRECT_TO_VM_INITIATED";
public static final String DIRECT_TO_VM_FINISHED = "DIRECT_TO_VM_FINISHED";
public static final String FILTERING_INITIATED = "FILTERING_INITIATED";
+ public static final String DND_PRE_CHECK_INITIATED = "DND_PRE_CHECK_INITIATED";
+ public static final String DND_PRE_CHECK_COMPLETED = "DND_PRE_CHECK_COMPLETED";
public static final String FILTERING_COMPLETED = "FILTERING_COMPLETED";
public static final String FILTERING_TIMED_OUT = "FILTERING_TIMED_OUT";
public static final String REMOTELY_HELD = "REMOTELY_HELD";
@@ -209,6 +219,15 @@ public class LogUtils {
"CALL_DIAGNOSTIC_SERVICE_TIMEOUT";
public static final String VERSTAT_CHANGED = "VERSTAT_CHANGED";
public static final String SET_VOIP_MODE = "SET_VOIP_MODE";
+ public static final String STATE_TIMEOUT = "STATE_TIMEOUT";
+ public static final String ICS_EXTRAS_CHANGED = "ICS_EXTRAS_CHANGED";
+ public static final String FLASH_NOTIFICATION_START = "FLASH_NOTIFICATION_START";
+ public static final String FLASH_NOTIFICATION_STOP = "FLASH_NOTIFICATION_STOP";
+ public static final String GAINED_FGS_DELEGATION = "GAINED_FGS_DELEGATION";
+ public static final String GAIN_FGS_DELEGATION_FAILED = "GAIN_FGS_DELEGATION_FAILED";
+ public static final String LOST_FGS_DELEGATION = "LOST_FGS_DELEGATION";
+ public static final String START_STREAMING = "START_STREAMING";
+ public static final String STOP_STREAMING = "STOP_STREAMING";
public static class Timings {
public static final String ACCEPT_TIMING = "accept";
@@ -222,6 +241,7 @@ public class LogUtils {
public static final String DIRECT_TO_VM_FINISHED_TIMING = "direct_to_vm_finished";
public static final String BLOCK_CHECK_FINISHED_TIMING = "block_check_finished";
public static final String FILTERING_COMPLETED_TIMING = "filtering_completed";
+ public static final String DND_PRE_CHECK_COMPLETED_TIMING = "dnd_pre_check_completed";
public static final String FILTERING_TIMED_OUT_TIMING = "filtering_timed_out";
public static final String START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING =
"start_connection_to_request_disconnect";
@@ -243,6 +263,8 @@ public class LogUtils {
BLOCK_CHECK_FINISHED_TIMING),
new TimedEventPair(FILTERING_INITIATED, FILTERING_COMPLETED,
FILTERING_COMPLETED_TIMING),
+ new TimedEventPair(DND_PRE_CHECK_INITIATED, DND_PRE_CHECK_COMPLETED,
+ DND_PRE_CHECK_COMPLETED_TIMING),
new TimedEventPair(FILTERING_INITIATED, FILTERING_TIMED_OUT,
FILTERING_TIMED_OUT_TIMING, 6000L),
new TimedEventPair(START_CONNECTION, REQUEST_DISCONNECT,
diff --git a/src/com/android/server/telecom/MmiUtils.java b/src/com/android/server/telecom/MmiUtils.java
new file mode 100644
index 000000000..11f6d59b4
--- /dev/null
+++ b/src/com/android/server/telecom/MmiUtils.java
@@ -0,0 +1,184 @@
+/*
+ * 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 android.net.Uri;
+import android.telecom.PhoneAccount;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MmiUtils {
+ // See TS 22.030 6.5.2 "Structure of the MMI"
+
+ private static Pattern sPatternSuppService = Pattern.compile(
+ "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)");
+ /* 1 2 3 4 5 6 7 8 9 10 11
+ 12
+
+ 1 = Full string up to and including #
+ 2 = action (activation/interrogation/registration/erasure)
+ 3 = service code
+ 5 = SIA
+ 7 = SIB
+ 9 = SIC
+ 10 = dialing number
+ */
+ //regex groups
+ static final int MATCH_GROUP_POUND_STRING = 1;
+ static final int MATCH_GROUP_ACTION = 2; //(activation/interrogation/registration/erasure)
+ static final int MATCH_GROUP_SERVICE_CODE = 3;
+ static final int MATCH_GROUP_SIA = 5;
+ static final int MATCH_GROUP_SIB = 7;
+ static final int MATCH_GROUP_SIC = 9;
+ static final int MATCH_GROUP_PWD_CONFIRM = 11;
+ static final int MATCH_GROUP_DIALING_NUMBER = 12;
+ // Call Forwarding service codes
+ static final String SC_CFU = "21";
+ static final String SC_CFB = "67";
+ static final String SC_CFNRy = "61";
+ static final String SC_CFNR = "62";
+ static final String SC_CF_All = "002";
+ static final String SC_CF_All_Conditional = "004";
+
+ //see: https://nationalnanpa.com/number_resource_info/vsc_assignments.html
+ @SuppressWarnings("DoubleBraceInitialization")
+ private static Set<String> sDangerousVerticalServiceCodes = new HashSet<String>()
+ {{
+ add("*09"); //Selective Call Blocking/Reporting
+ add("*42"); //Change Forward-To Number for Cust Programmable Call Forwarding Don't Answer
+ add("*56"); //Change Forward-To Number for ISDN Call Forwarding
+ add("*60"); //Selective Call Rejection Activation
+ add("*63"); //Selective Call Forwarding Activation
+ add("*64"); //Selective Call Acceptance Activation
+ add("*68"); //Call Forwarding Busy Line/Don't Answer Activation
+ add("*72"); //Call Forwarding Activation
+ add("*77"); //Anonymous Call Rejection Activation
+ add("*78"); //Do Not Disturb Activation
+ }};
+ private final int mMinLenInDangerousSet;
+ private final int mMaxLenInDangerousSet;
+
+ public MmiUtils() {
+ mMinLenInDangerousSet = sDangerousVerticalServiceCodes.stream()
+ .mapToInt(String::length)
+ .min()
+ .getAsInt();
+ mMaxLenInDangerousSet = sDangerousVerticalServiceCodes.stream()
+ .mapToInt(String::length)
+ .max()
+ .getAsInt();
+ }
+
+ /**
+ * Determines if the Uri represents a call forwarding related mmi code
+ *
+ * @param handle The URI to call.
+ * @return {@code True} if the URI represents a call forwarding related MMI
+ */
+ private static boolean isCallForwardingMmiCode(Uri handle) {
+ Matcher m;
+ String dialString = handle.getSchemeSpecificPart();
+ m = sPatternSuppService.matcher(dialString);
+
+ if (m.matches()) {
+ String sc = m.group(MATCH_GROUP_SERVICE_CODE);
+ return sc != null &&
+ (sc.equals(SC_CFU)
+ || sc.equals(SC_CFB) || sc.equals(SC_CFNRy)
+ || sc.equals(SC_CFNR) || sc.equals(SC_CF_All)
+ || sc.equals(SC_CF_All_Conditional));
+ }
+
+ return false;
+
+ }
+
+ private static boolean isTelScheme(Uri handle) {
+ return (handle != null && handle.getSchemeSpecificPart() != null &&
+ handle.getScheme() != null &&
+ handle.getScheme().equals(PhoneAccount.SCHEME_TEL));
+ }
+
+ private boolean isDangerousVerticalServiceCode(Uri handle) {
+ if (isTelScheme(handle)) {
+ String dialedNumber = handle.getSchemeSpecificPart();
+ if (dialedNumber.length() >= mMinLenInDangerousSet && dialedNumber.charAt(0) == '*') {
+ //we only check vertical codes defined by The North American Numbering Plan Admin
+ //see: https://nationalnanpa.com/number_resource_info/vsc_assignments.html
+ //only two or 3-digit codes are valid as of today, but the code is generic enough.
+ for (int prefixLen = mMaxLenInDangerousSet; prefixLen <= mMaxLenInDangerousSet;
+ prefixLen++) {
+ String prefix = dialedNumber.substring(0, prefixLen);
+ if (sDangerousVerticalServiceCodes.contains(prefix)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines if a dialed number is potentially an In-Call MMI code. In-Call MMI codes are
+ * MMI codes which can be dialed when one or more calls are in progress.
+ * <P>
+ * Checks for numbers formatted similar to the MMI codes defined in:
+ * {@link com.android.internal.telephony.Phone#handleInCallMmiCommands(String)}
+ *
+ * @param handle The URI to call.
+ * @return {@code True} if the URI represents a number which could be an in-call MMI code.
+ */
+ public boolean isPotentialInCallMMICode(Uri handle) {
+ if (isTelScheme(handle)) {
+ String dialedNumber = handle.getSchemeSpecificPart();
+ return (dialedNumber.equals("0") ||
+ (dialedNumber.startsWith("1") && dialedNumber.length() <= 2) ||
+ (dialedNumber.startsWith("2") && dialedNumber.length() <= 2) ||
+ dialedNumber.equals("3") ||
+ dialedNumber.equals("4") ||
+ dialedNumber.equals("5"));
+ }
+ return false;
+ }
+
+ public boolean isPotentialMMICode(Uri handle) {
+ return (handle != null && handle.getSchemeSpecificPart() != null
+ && handle.getSchemeSpecificPart().contains("#"));
+ }
+
+ /**
+ * Determines if the Uri represents a dangerous MMI code or Vertical Service code. Dangerous
+ * codes are ones, for which,
+ * we normally expect the user to be aware that an application has dialed them
+ *
+ * @param handle The URI to call.
+ * @return {@code True} if the URI represents a dangerous code
+ */
+ public boolean isDangerousMmiOrVerticalCode(Uri handle) {
+ if (isPotentialMMICode(handle)) {
+ return isCallForwardingMmiCode(handle);
+ //since some dangerous mmi codes could be carrier specific, in the future,
+ //we can add a carrier config item which can list carrier specific dangerous mmi codes
+ } else if (isDangerousVerticalServiceCode(handle)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 4569950e2..3b402b1aa 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -16,14 +16,12 @@
package com.android.server.telecom;
-import android.app.AppOpsManager;
-
import android.app.Activity;
+import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Trace;
@@ -78,6 +76,7 @@ public class NewOutgoingCallIntentBroadcaster {
private final PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
private final TelecomSystem.SyncRoot mLock;
private final DefaultDialerCache mDefaultDialerCache;
+ private final MmiUtils mMmiUtils;
/*
* Whether or not the outgoing call intent originated from the default phone application. If
@@ -101,7 +100,7 @@ public class NewOutgoingCallIntentBroadcaster {
@VisibleForTesting
public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager,
Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
- boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache) {
+ boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils) {
mContext = context;
mCallsManager = callsManager;
mIntent = intent;
@@ -109,6 +108,7 @@ public class NewOutgoingCallIntentBroadcaster {
mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
mLock = mCallsManager.getLock();
mDefaultDialerCache = defaultDialerCache;
+ mMmiUtils = mmiUtils;
}
/**
@@ -139,7 +139,7 @@ public class NewOutgoingCallIntentBroadcaster {
disconnectTimeout = getDisconnectTimeoutFromApp(
getResultExtras(false), disconnectTimeout);
endEarly = true;
- } else if (isPotentialEmergencyNumber(resultNumber)) {
+ } else if (isEmergencyNumber(resultNumber)) {
Log.w(this, "Cannot modify outgoing call to emergency number %s.",
resultNumber);
disconnectTimeout = 0;
@@ -273,14 +273,14 @@ public class NewOutgoingCallIntentBroadcaster {
return result;
}
- final boolean isPotentialEmergencyNumber = isPotentialEmergencyNumber(number);
- Log.v(this, "isPotentialEmergencyNumber = %s", isPotentialEmergencyNumber);
+ final boolean isEmergencyNumber = isEmergencyNumber(number);
+ Log.v(this, "isEmergencyNumber = %s", isEmergencyNumber);
- action = calculateCallIntentAction(intent, isPotentialEmergencyNumber);
+ action = calculateCallIntentAction(intent, isEmergencyNumber);
intent.setAction(action);
if (Intent.ACTION_CALL.equals(action)) {
- if (isPotentialEmergencyNumber) {
+ if (isEmergencyNumber) {
if (!mIsDefaultOrSystemPhoneApp) {
Log.w(this, "Cannot call potential emergency number %s with CALL Intent %s "
+ "unless caller is system or default dialer.", number, intent);
@@ -291,9 +291,19 @@ public class NewOutgoingCallIntentBroadcaster {
result.callImmediately = true;
result.requestRedirection = false;
}
+ } else if (mMmiUtils.isDangerousMmiOrVerticalCode(intent.getData())) {
+ if (!mIsDefaultOrSystemPhoneApp) {
+ Log.w(this,
+ "Potentially dangerous MMI code %s with CALL Intent %s can only be "
+ + "sent if caller is the system or default dialer",
+ number, intent);
+ launchSystemDialer(intent.getData());
+ result.disconnectCause = DisconnectCause.OUTGOING_CANCELED;
+ return result;
+ }
}
} else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
- if (!isPotentialEmergencyNumber) {
+ if (!isEmergencyNumber) {
Log.w(this, "Cannot call non-potential-emergency number %s with EMERGENCY_CALL "
+ "Intent %s.", number, intent);
result.disconnectCause = DisconnectCause.OUTGOING_CANCELED;
@@ -372,14 +382,14 @@ public class NewOutgoingCallIntentBroadcaster {
* broadcasting.
*/
callRedirectionWithService = callRedirectionProcessor
- .canMakeCallRedirectionWithService();
+ .canMakeCallRedirectionWithServiceAsUser(mCall.getAssociatedUser());
if (callRedirectionWithService) {
- callRedirectionProcessor.performCallRedirection();
+ callRedirectionProcessor.performCallRedirection(mCall.getAssociatedUser());
}
}
if (disposition.sendBroadcast) {
- UserHandle targetUser = mCall.getInitiatingUser();
+ UserHandle targetUser = mCall.getAssociatedUser();
Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
broadcastIntent(mIntent, disposition.number,
!disposition.callImmediately && !callRedirectionWithService, targetUser);
@@ -517,23 +527,18 @@ public class NewOutgoingCallIntentBroadcaster {
* that only the CALL_PRIVILEGED and CALL_EMERGENCY intents are allowed to make emergency
* calls.
*
- * To prevent malicious 3rd party apps from making emergency calls by passing in an
- * "invalid" number like "9111234" (that isn't technically an emergency number but might
- * still result in an emergency call with some networks), we use
- * isPotentialLocalEmergencyNumber instead of isLocalEmergencyNumber.
- *
* @param number number to inspect in order to determine whether or not an emergency number
- * is potentially being dialed
- * @return True if the handle is potentially an emergency number.
+ * is being dialed
+ * @return True if the handle is an emergency number.
*/
- private boolean isPotentialEmergencyNumber(String number) {
+ private boolean isEmergencyNumber(String number) {
Log.v(this, "Checking restrictions for number : %s", Log.pii(number));
if (number == null) return false;
try {
- return mContext.getSystemService(TelephonyManager.class).isPotentialEmergencyNumber(
+ return mContext.getSystemService(TelephonyManager.class).isEmergencyNumber(
number);
} catch (Exception e) {
- Log.e(this, e, "isPotentialEmergencyNumber: Telephony threw an exception.");
+ Log.e(this, e, "isEmergencyNumber: Telephony threw an exception.");
return false;
}
}
@@ -543,17 +548,17 @@ public class NewOutgoingCallIntentBroadcaster {
* the appropriate call intent action.
*
* @param intent Intent to evaluate
- * @param isPotentialEmergencyNumber Whether or not the number is potentially an emergency
+ * @param isEmergencyNumber Whether or not the number is an emergency
* number.
* @return The appropriate action.
*/
- private String calculateCallIntentAction(Intent intent, boolean isPotentialEmergencyNumber) {
+ private String calculateCallIntentAction(Intent intent, boolean isEmergencyNumber) {
String action = intent.getAction();
/* Change CALL_PRIVILEGED into CALL or CALL_EMERGENCY as needed. */
if (Intent.ACTION_CALL_PRIVILEGED.equals(action)) {
- if (isPotentialEmergencyNumber) {
- Log.i(this, "ACTION_CALL_PRIVILEGED is used while the number is a potential"
+ if (isEmergencyNumber) {
+ Log.i(this, "ACTION_CALL_PRIVILEGED is used while the number is a"
+ " emergency number. Using ACTION_CALL_EMERGENCY as an action instead.");
action = Intent.ACTION_CALL_EMERGENCY;
} else {
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index 0becaef30..673b99a9b 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -196,6 +196,8 @@ public class ParcelableCallUtils {
String callerDisplayName = call.getCallerDisplayNamePresentation() ==
TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null;
+ Uri contactPhotoUri = call.getContactPhotoUri();
+
List<Call> conferenceableCalls = call.getConferenceableCalls();
List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
for (Call otherCall : conferenceableCalls) {
@@ -255,6 +257,7 @@ public class ParcelableCallUtils {
.setCallerNumberVerificationStatus(call.getCallerNumberVerificationStatus())
.setContactDisplayName(call.getName())
.setActiveChildCallId(activeChildCallId)
+ .setContactPhotoUri(contactPhotoUri)
.createParcelableCall();
}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 576a2896e..acf07e35a 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -32,6 +32,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Icon;
import android.net.Uri;
+import android.os.Binder;
import android.os.Bundle;
import android.os.AsyncTask;
import android.os.PersistableBundle;
@@ -41,7 +42,6 @@ import android.os.UserManager;
import android.provider.Settings;
import android.telecom.CallAudioState;
import android.telecom.ConnectionService;
-import android.telecom.DefaultDialerManager;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -58,15 +58,14 @@ import android.util.Xml;
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.ModifiedUtf8;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
-import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -85,12 +84,9 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.stream.Collector;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* Handles writing and reading PhoneAccountHandle registration entries. This is a simple verbatim
@@ -157,9 +153,14 @@ public class PhoneAccountRegistrar {
};
public static final String FILE_NAME = "phone-account-registrar-state.xml";
+ public static final String ICON_ERROR_MSG =
+ "Icon cannot be written to memory. Try compressing or downsizing";
@VisibleForTesting
public static final int EXPECTED_STATE_VERSION = 9;
public static final int MAX_PHONE_ACCOUNT_REGISTRATIONS = 10;
+ public static final int MAX_PHONE_ACCOUNT_EXTRAS_KEY_PAIR_LIMIT = 100;
+ public static final int MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT = 256;
+ public static final int MAX_SCHEMES_PER_ACCOUNT = 10;
/** Keep in sync with the same in SipSettings.java */
private static final String SIP_SHARED_PREFERENCES = "SIP_PREFERENCES";
@@ -204,6 +205,7 @@ public class PhoneAccountRegistrar {
// register context based receiver to clean up orphan phone accounts
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MANAGED_PROFILE_REMOVED);
+ intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiver(mManagedProfileReceiver, intentFilter);
read();
@@ -248,7 +250,7 @@ public class PhoneAccountRegistrar {
}
List<PhoneAccountHandle> outgoing = getCallCapablePhoneAccounts(uriScheme, false,
- userHandle);
+ userHandle, false);
switch (outgoing.size()) {
case 0:
// There are no accounts, so there can be no default
@@ -323,7 +325,7 @@ public class PhoneAccountRegistrar {
}
// Get the PhoneAccount with the same group Id (and same ComponentName) that is not the
// newAccount that was just added
- List<PhoneAccount> accounts = getAllPhoneAccounts(userHandle).stream()
+ List<PhoneAccount> accounts = getAllPhoneAccounts(userHandle, false).stream()
.filter(account -> groupId.equals(account.getGroupId()) &&
!account.getAccountHandle().equals(excludePhoneAccountHandle) &&
Objects.equals(account.getAccountHandle().getComponentName(),
@@ -469,7 +471,7 @@ public class PhoneAccountRegistrar {
// loop through and look for any connection manager in the same package.
List<PhoneAccountHandle> allSimCallManagers = getPhoneAccountHandles(
PhoneAccount.CAPABILITY_CONNECTION_MANAGER, null, null,
- true /* includeDisabledAccounts */, userHandle);
+ true /* includeDisabledAccounts */, userHandle, false);
for (PhoneAccountHandle accountHandle : allSimCallManagers) {
ComponentName component = accountHandle.getComponentName();
@@ -561,10 +563,7 @@ public class PhoneAccountRegistrar {
if (call == null) {
return null;
}
- UserHandle userHandle = call.getInitiatingUser();
- if (userHandle == null) {
- userHandle = call.getTargetPhoneAccount().getUserHandle();
- }
+ UserHandle userHandle = call.getAssociatedUser();
PhoneAccountHandle targetPhoneAccount = call.getTargetPhoneAccount();
Log.d(this, "getSimCallManagerFromCall: callId=%s, targetPhac=%s",
call.getId(), targetPhoneAccount);
@@ -725,16 +724,13 @@ public class PhoneAccountRegistrar {
*
* @return The list of {@link PhoneAccountHandle}s.
*/
- public List<PhoneAccountHandle> getAllPhoneAccountHandles(UserHandle userHandle) {
- return getPhoneAccountHandles(0, null, null, false, userHandle);
+ public List<PhoneAccountHandle> getAllPhoneAccountHandles(UserHandle userHandle,
+ boolean crossUserAccess) {
+ return getPhoneAccountHandles(0, null, null, false, userHandle, crossUserAccess);
}
- public List<PhoneAccount> getAllPhoneAccounts(UserHandle userHandle) {
- return getPhoneAccounts(0, null, null, false, userHandle);
- }
-
- public List<PhoneAccount> getAllPhoneAccountsOfCurrentUser() {
- return getAllPhoneAccounts(mCurrentUserHandle);
+ public List<PhoneAccount> getAllPhoneAccounts(UserHandle userHandle, boolean crossUserAccess) {
+ return getPhoneAccounts(0, null, null, false, mCurrentUserHandle, crossUserAccess);
}
/**
@@ -748,9 +744,11 @@ public class PhoneAccountRegistrar {
* @return The phone account handles.
*/
public List<PhoneAccountHandle> getCallCapablePhoneAccounts(
- String uriScheme, boolean includeDisabledAccounts, UserHandle userHandle) {
+ String uriScheme, boolean includeDisabledAccounts,
+ UserHandle userHandle, boolean crossUserAccess) {
return getCallCapablePhoneAccounts(uriScheme, includeDisabledAccounts, userHandle,
- 0 /* capabilities */, PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY);
+ 0 /* capabilities */, PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY,
+ crossUserAccess);
}
/**
@@ -767,11 +765,11 @@ public class PhoneAccountRegistrar {
*/
public List<PhoneAccountHandle> getCallCapablePhoneAccounts(
String uriScheme, boolean includeDisabledAccounts, UserHandle userHandle,
- int capabilities, int excludedCapabilities) {
+ int capabilities, int excludedCapabilities, boolean crossUserAccess) {
return getPhoneAccountHandles(
PhoneAccount.CAPABILITY_CALL_PROVIDER | capabilities,
excludedCapabilities /*excludedCapabilities*/,
- uriScheme, null, includeDisabledAccounts, userHandle);
+ uriScheme, null, includeDisabledAccounts, userHandle, crossUserAccess);
}
/**
@@ -789,12 +787,7 @@ public class PhoneAccountRegistrar {
PhoneAccount.CAPABILITY_SELF_MANAGED,
PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY /* excludedCapabilities */,
null /* uriScheme */, null /* packageName */, false /* includeDisabledAccounts */,
- userHandle);
- }
-
- public List<PhoneAccountHandle> getCallCapablePhoneAccountsOfCurrentUser(
- String uriScheme, boolean includeDisabledAccounts) {
- return getCallCapablePhoneAccounts(uriScheme, includeDisabledAccounts, mCurrentUserHandle);
+ userHandle, false);
}
/**
@@ -803,7 +796,7 @@ public class PhoneAccountRegistrar {
public List<PhoneAccountHandle> getSimPhoneAccounts(UserHandle userHandle) {
return getPhoneAccountHandles(
PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION,
- null, null, false, userHandle);
+ null, null, false, userHandle, false);
}
public List<PhoneAccountHandle> getSimPhoneAccountsOfCurrentUser() {
@@ -818,7 +811,17 @@ public class PhoneAccountRegistrar {
*/
public List<PhoneAccountHandle> getPhoneAccountsForPackage(String packageName,
UserHandle userHandle) {
- return getPhoneAccountHandles(0, null, packageName, false, userHandle);
+ return getPhoneAccountHandles(0, null, packageName, false, userHandle, false);
+ }
+
+
+ /**
+ * includes disabled, includes crossUserAccess
+ */
+ public List<PhoneAccountHandle> getAllPhoneAccountHandlesForPackage(UserHandle userHandle,
+ String packageName) {
+ return getPhoneAccountHandles(0, null, packageName, true /* includeDisabled */, userHandle,
+ true /* crossUserAccess */);
}
/**
@@ -859,34 +862,190 @@ public class PhoneAccountRegistrar {
* Performs checks before calling addOrReplacePhoneAccount(PhoneAccount)
*
* @param account The {@code PhoneAccount} to add or replace.
- * @throws SecurityException if package does not have BIND_TELECOM_CONNECTION_SERVICE permission
+ * @throws SecurityException if package does not have BIND_TELECOM_CONNECTION_SERVICE
+ * permission
* @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached
+ * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT is reached
+ * @throws IllegalArgumentException if writing the Icon to memory will cause an Exception
*/
public void registerPhoneAccount(PhoneAccount account) {
// Enforce the requirement that a connection service for a phone account has the correct
// permission.
- if (!phoneAccountRequiresBindPermission(account.getAccountHandle())) {
+ if (!hasTransactionalCallCapabilities(account) &&
+ !phoneAccountRequiresBindPermission(account.getAccountHandle())) {
Log.w(this,
"Phone account %s does not have BIND_TELECOM_CONNECTION_SERVICE permission.",
account.getAccountHandle());
- throw new SecurityException("PhoneAccount connection service requires "
- + "BIND_TELECOM_CONNECTION_SERVICE permission.");
- }
- //Enforce an upper bound on the number of PhoneAccount's a package can register.
- // Most apps should only require 1-2.
- if (getPhoneAccountsForPackage(
- account.getAccountHandle().getComponentName().getPackageName(),
- account.getAccountHandle().getUserHandle()).size()
+ throw new SecurityException("Registering a PhoneAccount requires either: "
+ + "(1) The Service definition requires that the ConnectionService is guarded"
+ + " with the BIND_TELECOM_CONNECTION_SERVICE, which can be defined using the"
+ + " android:permission tag as part of the Service definition. "
+ + "(2) The PhoneAccount capability called"
+ + " CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS.");
+ }
+ enforceCharacterLimit(account);
+ enforceIconSizeLimit(account);
+ enforceMaxPhoneAccountLimit(account);
+ addOrReplacePhoneAccount(account);
+ }
+
+ /**
+ * Enforce an upper bound on the number of PhoneAccount's a package can register.
+ * Most apps should only require 1-2. * Include disabled accounts.
+ *
+ * @param account to enforce check on
+ * @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) {
- Log.w(this, "Phone account %s reached max registration limit for package",
- account.getAccountHandle());
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceMaxPhoneAccountLimit");
throw new IllegalArgumentException(
"Error, cannot register phone account " + account.getAccountHandle()
+ " because the limit, " + MAX_PHONE_ACCOUNT_REGISTRATIONS
+ ", has been reached");
}
+ }
- addOrReplacePhoneAccount(account);
+ /**
+ * determine if there will be an issue writing the icon to memory
+ *
+ * @param account to enforce check on
+ * @throws IllegalArgumentException if writing the Icon to memory will cause an Exception
+ */
+ @VisibleForTesting
+ public void enforceIconSizeLimit(PhoneAccount account) {
+ if (account.getIcon() == null) {
+ return;
+ }
+ String text = "";
+ // convert the icon into a Base64 String
+ try {
+ text = XmlSerialization.writeIconToBase64String(account.getIcon());
+ } catch (IOException e) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceIconSizeLimit");
+ throw new IllegalArgumentException(ICON_ERROR_MSG);
+ }
+ // enforce the max bytes check in com.android.modules.utils.FastDataOutput#writeUTF(string)
+ try {
+ final int len = (int) ModifiedUtf8.countBytes(text, false);
+ if (len > 65_535 /* MAX_UNSIGNED_SHORT */) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceIconSizeLimit");
+ throw new IllegalArgumentException(ICON_ERROR_MSG);
+ }
+ } catch (IOException e) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceIconSizeLimit");
+ throw new IllegalArgumentException(ICON_ERROR_MSG);
+ }
+ }
+
+ /**
+ * All {@link PhoneAccount} and{@link PhoneAccountHandle} String and Char-Sequence fields
+ * should be restricted to character limit of MAX_PHONE_ACCOUNT_CHAR_LIMIT to prevent exceptions
+ * when writing large character streams to XML-Serializer.
+ *
+ * @param account to enforce character limit checks on
+ * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT reached
+ */
+ public void enforceCharacterLimit(PhoneAccount account) {
+ if (account == null) {
+ return;
+ }
+ PhoneAccountHandle handle = account.getAccountHandle();
+
+ String[] fields =
+ {"Package Name", "Class Name", "PhoneAccountHandle Id", "Label", "ShortDescription",
+ "GroupId", "Address", "SubscriptionAddress"};
+ CharSequence[] args = {handle.getComponentName().getPackageName(),
+ handle.getComponentName().getClassName(), handle.getId(), account.getLabel(),
+ account.getShortDescription(), account.getGroupId(),
+ (account.getAddress() != null ? account.getAddress().toString() : ""),
+ (account.getSubscriptionAddress() != null ?
+ account.getSubscriptionAddress().toString() : "")};
+
+ for (int i = 0; i < fields.length; i++) {
+ if (args[i] != null && args[i].length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceCharacterLimit");
+ throw new IllegalArgumentException("The PhoneAccount or PhoneAccountHandle ["
+ + fields[i] + "] field has an invalid character count. PhoneAccount and "
+ + "PhoneAccountHandle String and Char-Sequence fields are limited to "
+ + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT + " characters.");
+ }
+ }
+
+ // Enforce limits on the URI Schemes provided
+ enforceLimitsOnSchemes(account);
+
+ // Enforce limit on the PhoneAccount#mExtras
+ Bundle extras = account.getExtras();
+ if (extras != null) {
+ if (extras.keySet().size() > MAX_PHONE_ACCOUNT_EXTRAS_KEY_PAIR_LIMIT) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceCharacterLimit");
+ throw new IllegalArgumentException("The PhoneAccount#mExtras is limited to " +
+ MAX_PHONE_ACCOUNT_EXTRAS_KEY_PAIR_LIMIT + " (key,value) pairs.");
+ }
+
+ for (String key : extras.keySet()) {
+ Object value = extras.get(key);
+
+ if ((key != null && key.length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) ||
+ (value instanceof String &&
+ ((String) value).length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT)) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceCharacterLimit");
+ throw new IllegalArgumentException("The PhoneAccount#mExtras contains a String"
+ + " key or value that has an invalid character count. PhoneAccount and "
+ + "PhoneAccountHandle String and Char-Sequence fields are limited to "
+ + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT + " characters.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Enforce a character limit on all PA and PAH string or char-sequence fields.
+ *
+ * @param account to enforce check on
+ * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT reached
+ */
+ @VisibleForTesting
+ public void enforceLimitsOnSchemes(@NonNull PhoneAccount account) {
+ List<String> schemes = account.getSupportedUriSchemes();
+
+ if (schemes == null) {
+ return;
+ }
+
+ if (schemes.size() > MAX_SCHEMES_PER_ACCOUNT) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceLimitsOnSchemes");
+ throw new IllegalArgumentException(
+ "Error, cannot register phone account " + account.getAccountHandle()
+ + " because the URI scheme limit of "
+ + MAX_SCHEMES_PER_ACCOUNT + " has been reached");
+ }
+
+ for (String scheme : schemes) {
+ if (scheme.length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+ EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
+ "enforceLimitsOnSchemes");
+ throw new IllegalArgumentException(
+ "Error, cannot register phone account " + account.getAccountHandle()
+ + " because the max scheme limit of "
+ + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT + " has been reached");
+ }
+ }
}
/**
@@ -904,6 +1063,15 @@ public class PhoneAccountRegistrar {
boolean isEnabled = false;
boolean isNewAccount;
+ // add self-managed capability for transactional accounts that are missing it
+ if (hasTransactionalCallCapabilities(account) &&
+ !account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+ account = account.toBuilder()
+ .setCapabilities(account.getCapabilities()
+ | PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .build();
+ }
+
PhoneAccount oldAccount = getPhoneAccountUnchecked(account.getAccountHandle());
if (oldAccount != null) {
enforceSelfManagedAccountUnmodified(account, oldAccount);
@@ -1184,7 +1352,11 @@ public class PhoneAccountRegistrar {
// 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) continue;
+ if (simTm == null) {
+ Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
+ + "simTm is null.");
+ continue;
+ }
simTm.setVoiceServiceStateOverride(hasService);
}
}
@@ -1202,6 +1374,7 @@ public class PhoneAccountRegistrar {
Log.w(this, "phoneAccount %s not found", phoneAccountHandle.getComponentName());
return false;
}
+
for (ResolveInfo resolveInfo : resolveInfos) {
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
if (serviceInfo == null) {
@@ -1220,6 +1393,15 @@ public class PhoneAccountRegistrar {
return true;
}
+ @VisibleForTesting
+ public boolean hasTransactionalCallCapabilities(PhoneAccount phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+ return phoneAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
+ }
+
//
// Methods for retrieving PhoneAccounts and PhoneAccountHandles
//
@@ -1266,9 +1448,10 @@ public class PhoneAccountRegistrar {
String uriScheme,
String packageName,
boolean includeDisabledAccounts,
- UserHandle userHandle) {
+ UserHandle userHandle,
+ boolean crossUserAccess) {
return getPhoneAccountHandles(capabilities, 0 /*excludedCapabilities*/, uriScheme,
- packageName, includeDisabledAccounts, userHandle);
+ packageName, includeDisabledAccounts, userHandle, crossUserAccess);
}
/**
@@ -1281,12 +1464,13 @@ public class PhoneAccountRegistrar {
String uriScheme,
String packageName,
boolean includeDisabledAccounts,
- UserHandle userHandle) {
+ UserHandle userHandle,
+ boolean crossUserAccess) {
List<PhoneAccountHandle> handles = new ArrayList<>();
for (PhoneAccount account : getPhoneAccounts(
capabilities, excludedCapabilities, uriScheme, packageName,
- includeDisabledAccounts, userHandle)) {
+ includeDisabledAccounts, userHandle, crossUserAccess)) {
handles.add(account.getAccountHandle());
}
return handles;
@@ -1297,9 +1481,10 @@ public class PhoneAccountRegistrar {
String uriScheme,
String packageName,
boolean includeDisabledAccounts,
- UserHandle userHandle) {
+ UserHandle userHandle,
+ boolean crossUserAccess) {
return getPhoneAccounts(capabilities, 0 /*excludedCapabilities*/, uriScheme, packageName,
- includeDisabledAccounts, userHandle);
+ includeDisabledAccounts, userHandle, crossUserAccess);
}
/**
@@ -1319,7 +1504,8 @@ public class PhoneAccountRegistrar {
String uriScheme,
String packageName,
boolean includeDisabledAccounts,
- UserHandle userHandle) {
+ UserHandle userHandle,
+ boolean crossUserAccess) {
List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
for (PhoneAccount m : mState.accounts) {
if (!(m.isEnabled() || includeDisabledAccounts)) {
@@ -1342,7 +1528,10 @@ public class PhoneAccountRegistrar {
}
PhoneAccountHandle handle = m.getAccountHandle();
- if (resolveComponent(handle).isEmpty()) {
+ // PhoneAccounts with CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS do not require a
+ // ConnectionService and will fail [resolveComponent(PhoneAccountHandle)]. Bypass
+ // the [resolveComponent(PhoneAccountHandle)] for transactional accounts.
+ if (!hasTransactionalCallCapabilities(m) && resolveComponent(handle).isEmpty()) {
// This component cannot be resolved anymore; skip this one.
continue;
}
@@ -1351,7 +1540,7 @@ public class PhoneAccountRegistrar {
// Not the right package name; skip this one.
continue;
}
- if (!isVisibleForUser(m, userHandle, false)) {
+ if (!crossUserAccess && !isVisibleForUser(m, userHandle, false)) {
// Account is not visible for the current user; skip this one.
continue;
}
@@ -1778,17 +1967,20 @@ public class PhoneAccountRegistrar {
protected void writeIconIfNonNull(String tagName, Icon value, XmlSerializer serializer)
throws IOException {
if (value != null) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- value.writeToStream(stream);
- byte[] iconByteArray = stream.toByteArray();
- String text = Base64.encodeToString(iconByteArray, 0, iconByteArray.length, 0);
-
+ String text = writeIconToBase64String(value);
serializer.startTag(null, tagName);
serializer.text(text);
serializer.endTag(null, tagName);
}
}
+ public static String writeIconToBase64String(Icon icon) throws IOException {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ icon.writeToStream(stream);
+ byte[] iconByteArray = stream.toByteArray();
+ return Base64.encodeToString(iconByteArray, 0, iconByteArray.length, 0);
+ }
+
protected void writeLong(String tagName, long value, XmlSerializer serializer)
throws IOException {
serializer.startTag(null, tagName);
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 850770332..1d42db46c 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -215,8 +215,9 @@ public class RespondViaSmsManager extends CallsManagerListenerBase {
MessageSentReceiver receiver = new MessageSentReceiver(
!TextUtils.isEmpty(contactName) ? contactName : phoneNumber,
messageParts.size());
- context.registerReceiver(receiver, new IntentFilter(ACTION_MESSAGE_SENT),
- Context.RECEIVER_NOT_EXPORTED);
+ IntentFilter messageSentFilter = new IntentFilter(ACTION_MESSAGE_SENT);
+ messageSentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(receiver, messageSentFilter, Context.RECEIVER_NOT_EXPORTED);
smsManager.sendMultipartTextMessage(phoneNumber, null, messageParts,
sentIntents/*sentIntent*/, null /*deliveryIntent*/, context.getOpPackageName(),
context.getAttributionTag());
diff --git a/src/com/android/server/telecom/RespondViaSmsSettings.java b/src/com/android/server/telecom/RespondViaSmsSettings.java
index 661038bbc..d038a6e3c 100755
--- a/src/com/android/server/telecom/RespondViaSmsSettings.java
+++ b/src/com/android/server/telecom/RespondViaSmsSettings.java
@@ -89,6 +89,8 @@ public class RespondViaSmsSettings extends PreferenceActivity
if (actionBar != null) {
// android.R.id.home will be triggered in onOptionsItemSelected()
actionBar.setDisplayHomeAsUpEnabled(true);
+ // set the talkback voice prompt to "Back" instead of "Navigate Up"
+ actionBar.setHomeActionContentDescription(R.string.back);
}
}
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index c859fde80..171060420 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -21,6 +21,7 @@ import static android.provider.CallLog.Calls.USER_MISSED_LOW_RING_VOLUME;
import static android.provider.CallLog.Calls.USER_MISSED_NO_VIBRATE;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Person;
@@ -32,11 +33,14 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.telecom.Log;
import android.telecom.TelecomManager;
+import android.view.accessibility.AccessibilityManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.LogUtils.EventTimer;
@@ -47,12 +51,24 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
/**
* Controls the ringtone player.
*/
@VisibleForTesting
public class Ringer {
+ public interface AccessibilityManagerAdapter {
+ boolean startFlashNotificationSequence(@NonNull Context context,
+ @AccessibilityManager.FlashNotificationReason int reason);
+ boolean stopFlashNotificationSequence(@NonNull Context context);
+ }
+ /**
+ * Flag only for local debugging. Do not submit enabled.
+ */
+ private static final boolean DEBUG_RINGER = false;
+
public static class VibrationEffectProxy {
public VibrationEffect createWaveform(long[] timings, int[] amplitudes, int repeat) {
return VibrationEffect.createWaveform(timings, amplitudes, repeat);
@@ -152,15 +168,15 @@ public class Ringer {
*/
private CompletableFuture<Void> mBlockOnRingingFuture = null;
- private CompletableFuture<Void> mVibrateFuture = CompletableFuture.completedFuture(null);
-
private InCallTonePlayer mCallWaitingPlayer;
private RingtoneFactory mRingtoneFactory;
private AudioManager mAudioManager;
+ private NotificationManager mNotificationManager;
+ private AccessibilityManagerAdapter mAccessibilityManagerAdapter;
/**
* Call objects that are ringing, vibrating or call-waiting. These are used only for logging
- * purposes.
+ * purposes (except mVibratingCall is also used to ensure consistency).
*/
private Call mRingingCall;
private Call mVibratingCall;
@@ -189,7 +205,9 @@ public class Ringer {
RingtoneFactory ringtoneFactory,
Vibrator vibrator,
VibrationEffectProxy vibrationEffectProxy,
- InCallController inCallController) {
+ InCallController inCallController,
+ NotificationManager notificationManager,
+ AccessibilityManagerAdapter accessibilityManagerAdapter) {
mLock = new Object();
mSystemSettingsUtil = systemSettingsUtil;
@@ -202,6 +220,8 @@ public class Ringer {
mRingtoneFactory = ringtoneFactory;
mInCallController = inCallController;
mVibrationEffectProxy = vibrationEffectProxy;
+ mNotificationManager = notificationManager;
+ mAccessibilityManagerAdapter = accessibilityManagerAdapter;
if (mContext.getResources().getBoolean(R.bool.use_simple_vibration_pattern)) {
mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
@@ -213,6 +233,8 @@ public class Ringer {
mIsHapticPlaybackSupportedByDevice =
mSystemSettingsUtil.isHapticPlaybackSupported(mContext);
+
+ mAudioManager = mContext.getSystemService(AudioManager.class);
}
@VisibleForTesting
@@ -220,194 +242,304 @@ public class Ringer {
mBlockOnRingingFuture = future;
}
- public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {
- if (foregroundCall == null) {
- Log.wtf(this, "startRinging called with null foreground call.");
- return false;
- }
+ @VisibleForTesting
+ public void setNotificationManager(NotificationManager notificationManager) {
+ mNotificationManager = notificationManager;
+ }
- if (foregroundCall.getState() != CallState.RINGING
- && foregroundCall.getState() != CallState.SIMULATED_RINGING) {
- // Its possible for bluetooth to connect JUST as a call goes active, which would mean
- // the call would start ringing again.
- Log.i(this, "startRinging called for non-ringing foreground callid=%s",
- foregroundCall.getId());
- return false;
- }
+ public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {
+ boolean deferBlockOnRingingFuture = false;
+ // try-finally to ensure that the block on ringing future is always called.
+ try {
+ if (foregroundCall == null) {
+ Log.wtf(this, "startRinging called with null foreground call.");
+ return false;
+ }
- // Use completable future to establish a timeout, not intent to make these work outside the
- // main thread asynchronously
- // TODO: moving these RingerAttributes calculation out of Telecom lock to avoid blocking.
- CompletableFuture<RingerAttributes> ringerAttributesFuture = CompletableFuture
- .supplyAsync(() -> getRingerAttributes(foregroundCall, isHfpDeviceAttached),
- new LoggedHandlerExecutor(getHandler(), "R.sR", null));
+ if (foregroundCall.getState() != CallState.RINGING
+ && foregroundCall.getState() != CallState.SIMULATED_RINGING) {
+ // It's possible for bluetooth to connect JUST as a call goes active, which would
+ // mean the call would start ringing again.
+ Log.i(this, "startRinging called for non-ringing foreground callid=%s",
+ foregroundCall.getId());
+ return false;
+ }
- RingerAttributes attributes = null;
- try {
- mAttributesLatch = new CountDownLatch(1);
- attributes = ringerAttributesFuture.get(
- RINGER_ATTRIBUTES_TIMEOUT, TimeUnit.MILLISECONDS);
- } catch (ExecutionException | InterruptedException | TimeoutException e) {
- // Keep attributs as null
- Log.i(this, "getAttributes error: " + e);
- }
+ // Use completable future to establish a timeout, not intent to make these work outside
+ // the main thread asynchronously
+ // TODO: moving these RingerAttributes calculation out of Telecom lock to avoid blocking
+ CompletableFuture<RingerAttributes> ringerAttributesFuture = CompletableFuture
+ .supplyAsync(() -> getRingerAttributes(foregroundCall, isHfpDeviceAttached),
+ new LoggedHandlerExecutor(getHandler(), "R.sR", null));
- if (attributes == null) {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "RingerAttributes error");
- return false;
- }
+ RingerAttributes attributes = null;
+ try {
+ mAttributesLatch = new CountDownLatch(1);
+ attributes = ringerAttributesFuture.get(
+ RINGER_ATTRIBUTES_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ // Keep attributes as null
+ Log.i(this, "getAttributes error: " + e);
+ }
- if (attributes.isEndEarly()) {
- if (attributes.letDialerHandleRinging()) {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Dialer handles");
+ if (attributes == null) {
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
+ "RingerAttributes error");
+ return false;
}
- if (attributes.isSilentRingingRequested()) {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Silent ringing "
- + "requested");
+
+ if (attributes.isEndEarly()) {
+ boolean acquireAudioFocus = attributes.shouldAcquireAudioFocus();
+ if (attributes.letDialerHandleRinging()) {
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Dialer handles");
+ // Dialer will setup a ringtone, provide the audio focus if its audible.
+ acquireAudioFocus |= attributes.isRingerAudible();
+ }
+
+ if (attributes.isSilentRingingRequested()) {
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Silent ringing "
+ + "requested");
+ }
+ if (attributes.isWorkProfileInQuietMode()) {
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
+ "Work profile in quiet mode");
+ }
+ return acquireAudioFocus;
}
- if (mBlockOnRingingFuture != null) {
- mBlockOnRingingFuture.complete(null);
+
+ stopCallWaiting();
+
+ final boolean shouldFlash = attributes.shouldRingForContact();
+ if (mAccessibilityManagerAdapter != null && shouldFlash) {
+ Log.addEvent(foregroundCall, LogUtils.Events.FLASH_NOTIFICATION_START);
+ getHandler().post(() ->
+ mAccessibilityManagerAdapter.startFlashNotificationSequence(mContext,
+ AccessibilityManager.FLASH_REASON_CALL));
}
- return attributes.shouldAcquireAudioFocus();
- }
- stopCallWaiting();
-
- VibrationEffect effect;
- CompletableFuture<Boolean> hapticsFuture = null;
- // Determine if the settings and DND mode indicate that the vibrator can be used right now.
- boolean isVibratorEnabled = isVibratorEnabled(mContext, attributes.shouldRingForContact());
- boolean shouldApplyRampingRinger =
- isVibratorEnabled && mSystemSettingsUtil.isRampingRingerEnabled(mContext);
- if (attributes.isRingerAudible()) {
- mRingingCall = foregroundCall;
- Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);
- // Because we wait until a contact info query to complete before processing a
- // call (for the purposes of direct-to-voicemail), the information about custom
- // ringtones should be available by the time this code executes. We can safely
- // request the custom ringtone from the call and expect it to be current.
- if (shouldApplyRampingRinger) {
- Log.i(this, "start ramping ringer.");
- if (mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()) {
- effect = getVibrationEffectForCall(mRingtoneFactory, foregroundCall);
- } else {
- effect = mDefaultVibrationEffect;
- }
- if (mVolumeShaperConfig == null) {
+ // Determine if the settings and DND mode indicate that the vibrator can be used right
+ // now.
+ final boolean isVibratorEnabled =
+ isVibratorEnabled(mContext, attributes.shouldRingForContact());
+ boolean shouldApplyRampingRinger =
+ isVibratorEnabled && mSystemSettingsUtil.isRampingRingerEnabled(mContext);
+
+ boolean isHapticOnly = false;
+ boolean useCustomVibrationEffect = false;
+
+ mVolumeShaperConfig = null;
+
+ if (attributes.isRingerAudible()) {
+ mRingingCall = foregroundCall;
+ Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);
+ // Because we wait until a contact info query to complete before processing a
+ // call (for the purposes of direct-to-voicemail), the information about custom
+ // ringtones should be available by the time this code executes. We can safely
+ // request the custom ringtone from the call and expect it to be current.
+ if (shouldApplyRampingRinger) {
+ Log.i(this, "create ramping ringer.");
float silencePoint = (float) (RAMPING_RINGER_VIBRATION_DURATION)
/ (float) (RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION);
- mVolumeShaperConfig = new VolumeShaper.Configuration.Builder()
- .setDuration(
- RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION)
- .setCurve(new float[]{0.f, silencePoint + EPSILON /*keep monotonicity*/,
- 1.f}, new float[]{0.f, 0.f, 1.f})
- .setInterpolatorType(
- VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
- .build();
+ mVolumeShaperConfig =
+ new VolumeShaper.Configuration.Builder()
+ .setDuration(RAMPING_RINGER_VIBRATION_DURATION
+ + RAMPING_RINGER_DURATION)
+ .setCurve(
+ new float[]{0.f, silencePoint + EPSILON
+ /*keep monotonicity*/, 1.f},
+ new float[]{0.f, 0.f, 1.f})
+ .setInterpolatorType(
+ VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
+ .build();
+ if (mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()) {
+ useCustomVibrationEffect = true;
+ }
+ } else {
+ if (DEBUG_RINGER) {
+ Log.i(this, "Create ringer with custom vibration effect");
+ }
+ // Ramping ringtone is not enabled.
+ useCustomVibrationEffect = true;
}
- hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall,
- mVolumeShaperConfig, attributes.isRingerAudible(), isVibratorEnabled);
} else {
- // Ramping ringtone is not enabled.
- hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall, null,
- attributes.isRingerAudible(), isVibratorEnabled);
- effect = getVibrationEffectForCall(mRingtoneFactory, foregroundCall);
- }
- } else {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Inaudible: "
- + attributes.getInaudibleReason());
- if (isVibratorEnabled && mIsHapticPlaybackSupportedByDevice) {
- // Attempt to run the attentional haptic ringtone first and fallback to the default
- // vibration effect if hapticFuture is completed with false.
- hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall, null,
- attributes.isRingerAudible(), isVibratorEnabled);
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
+ "Inaudible: " + attributes.getInaudibleReason()
+ + " isVibratorEnabled=" + isVibratorEnabled);
+
+ if (isVibratorEnabled) {
+ // If ringer is not audible for this call, then the phone is in "Vibrate" mode.
+ // Use haptic-only ringtone or do not play anything.
+ isHapticOnly = true;
+ if (DEBUG_RINGER) {
+ Log.i(this, "Set ringtone as haptic only: " + isHapticOnly);
+ }
+ } else {
+ foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
+ return attributes.shouldAcquireAudioFocus(); // ringer not audible
+ }
}
- effect = mDefaultVibrationEffect;
- }
- if (hapticsFuture != null) {
- final boolean shouldRingForContact = attributes.shouldRingForContact();
- final boolean isRingerAudible = attributes.isRingerAudible();
- mVibrateFuture = hapticsFuture.thenAccept(isUsingAudioCoupledHaptics -> {
- if (!isUsingAudioCoupledHaptics || !mIsHapticPlaybackSupportedByDevice) {
- Log.i(this, "startRinging: fileHasHaptics=%b, hapticsSupported=%b",
- isUsingAudioCoupledHaptics, mIsHapticPlaybackSupportedByDevice);
- maybeStartVibration(foregroundCall, shouldRingForContact, effect,
- isVibratorEnabled, isRingerAudible);
- } else if (shouldApplyRampingRinger
- && !mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()) {
- Log.i(this, "startRinging: apply ramping ringer vibration");
- maybeStartVibration(foregroundCall, shouldRingForContact, effect,
- isVibratorEnabled, isRingerAudible);
+ boolean hapticChannelsMuted = !isVibratorEnabled || !mIsHapticPlaybackSupportedByDevice;
+ if (shouldApplyRampingRinger
+ && !mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()
+ && isVibratorEnabled) {
+ Log.i(this, "Muted haptic channels since audio coupled ramping ringer is disabled");
+ hapticChannelsMuted = true;
+ } else if (hapticChannelsMuted) {
+ Log.i(this,
+ "Muted haptic channels isVibratorEnabled=%s, hapticPlaybackSupported=%s",
+ isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
+ }
+ // Defer ringtone creation to the async player thread.
+ Supplier<Ringtone> ringtoneSupplier;
+ 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 {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
- "using audio-coupled haptics");
+ ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;
}
- });
- if (mBlockOnRingingFuture != null) {
- mVibrateFuture.whenComplete((v, e) -> mBlockOnRingingFuture.complete(null));
+ } else {
+ ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
+ foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
}
- } else {
- if (mBlockOnRingingFuture != null) {
+
+ // If vibration will be done, reserve the vibrator.
+ boolean vibratorReserved = isVibratorEnabled && attributes.shouldRingForContact()
+ && tryReserveVibration(foregroundCall);
+ 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);
+ }
+
+ // The vibration logic depends on the loaded ringtone, but we need to defer the ringtone
+ // load to the async ringtone thread. Hence, we bundle up the final part of this method
+ // for that thread to run after loading the ringtone. This logic is intended to run even
+ // 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) -> {
+ try {
+ 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.
+ return;
+ }
+ final VibrationEffect vibrationEffect;
+ if (ringtone != null && finalUseCustomVibrationEffect) {
+ if (DEBUG_RINGER) {
+ Log.d(this, "Using ringtone defined vibration effect.");
+ }
+ vibrationEffect = getVibrationEffectForRingtone(ringtone);
+ } else {
+ vibrationEffect = mDefaultVibrationEffect;
+ }
+
+ boolean isUsingAudioCoupledHaptics =
+ !finalHapticChannelsMuted && ringtone != null
+ && ringtone.hasHapticChannels();
+ vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect);
+ } finally {
+ // This is used to signal to tests that the async play() call has completed.
+ if (mBlockOnRingingFuture != null) {
+ mBlockOnRingingFuture.complete(null);
+ }
+ }
+ };
+ deferBlockOnRingingFuture = true; // Run in vibrationLogic.
+ if (ringtoneSupplier != null) {
+ mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic);
+ } else {
+ afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false);
+ }
+
+ // shouldAcquireAudioFocus is meant to be true, but that check is deferred to here
+ // because until now is when we actually know if the ringtone loading worked.
+ return attributes.shouldAcquireAudioFocus()
+ || (!isHapticOnly && attributes.isRingerAudible());
+ } finally {
+ // This is used to signal to tests that the async play() call has completed. It can
+ // be deferred into AsyncRingtonePlayer
+ if (mBlockOnRingingFuture != null && !deferBlockOnRingingFuture) {
mBlockOnRingingFuture.complete(null);
}
- Log.w(this, "startRinging: No haptics future; fallback to default behavior");
- maybeStartVibration(foregroundCall, attributes.shouldRingForContact(), effect,
- isVibratorEnabled, attributes.isRingerAudible());
}
-
- return attributes.shouldAcquireAudioFocus();
}
- private void maybeStartVibration(Call foregroundCall, boolean shouldRingForContact,
- VibrationEffect effect, boolean isVibrationEnabled, boolean isRingerAudible) {
+ /**
+ * Try to reserve the vibrator for this call, returning false if it's already committed.
+ * The vibration will be started by AsyncRingtonePlayer to ensure timing is aligned with the
+ * audio. The logic uses mVibratingCall to say which call is currently getting ready to vibrate,
+ * or actually vibrating (indicated by mIsVibrating).
+ *
+ * Once reserved, the vibrateIfNeeded method is expected to be called. Note that if
+ * audio-coupled haptics were used instead of vibrator, the reservation still stays until
+ * ringing is stopped, because the vibrator is exclusive to a single vibration source.
+ *
+ * Note that this "reservation" is only local to the Ringer - it's not locking the vibrator, so
+ * if it's busy with some other important vibration, this ringer's one may not displace it.
+ */
+ private boolean tryReserveVibration(Call foregroundCall) {
synchronized (mLock) {
- mAudioManager = mContext.getSystemService(AudioManager.class);
- if (isVibrationEnabled && !mIsVibrating && shouldRingForContact) {
+ if (mVibratingCall != null || mIsVibrating) {
+ return false;
+ }
+ mVibratingCall = foregroundCall;
+ return true;
+ }
+ }
+
+ private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, Call foregroundCall,
+ VibrationEffect effect) {
+ if (isUsingAudioCoupledHaptics) {
+ Log.addEvent(
+ foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using audio-coupled haptics");
+ return;
+ }
+
+ synchronized (mLock) {
+ // Ensure the reservation is live. The mIsVibrating check should be redundant.
+ if (foregroundCall == mVibratingCall && !mIsVibrating) {
Log.addEvent(foregroundCall, LogUtils.Events.START_VIBRATOR,
- "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b",
- mVibrator.hasVibrator(),
- mSystemSettingsUtil.isRingVibrationEnabled(mContext),
- mAudioManager.getRingerMode(), mIsVibrating);
- if (mSystemSettingsUtil.isRampingRingerEnabled(mContext) && isRingerAudible) {
- Log.i(this, "start vibration for ramping ringer.");
- } else {
- Log.i(this, "start normal vibration.");
- }
+ "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b",
+ mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext),
+ mAudioManager.getRingerMode(), mIsVibrating);
mIsVibrating = true;
mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
- } else {
- foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
- "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b",
- mVibrator.hasVibrator(),
- mSystemSettingsUtil.isRingVibrationEnabled(mContext),
- mAudioManager.getRingerMode(), mIsVibrating);
+ Log.i(this, "start vibration.");
}
+ // else stopped already: this isn't started unless a reservation was made.
}
}
- private VibrationEffect getVibrationEffectForCall(RingtoneFactory factory, Call call) {
- VibrationEffect effect = null;
- Ringtone ringtone = factory.getRingtone(call);
- Uri ringtoneUri = ringtone != null ? ringtone.getUri() : null;
- if (ringtoneUri != null) {
- try {
- effect = mVibrationEffectProxy.get(ringtoneUri, mContext);
- } catch (IllegalArgumentException iae) {
- // Deep in the bowels of the VibrationEffect class it is possible for an
- // IllegalArgumentException to be thrown if there is an invalid URI specified in the
- // device config, or a content provider failure. Rather than crashing the Telecom
- // process we will just use the default vibration effect.
- Log.e(this, iae, "getVibrationEffectForCall: failed to get vibration effect");
- effect = null;
- }
+ private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
+ Uri ringtoneUri = ringtone.getUri();
+ if (ringtoneUri == null) {
+ return mDefaultVibrationEffect;
}
-
- if (effect == null) {
- effect = mDefaultVibrationEffect;
+ try {
+ VibrationEffect effect = mVibrationEffectProxy.get(ringtoneUri, mContext);
+ if (effect == null) {
+ Log.i(this, "did not find vibration effect, falling back to default vibration");
+ return mDefaultVibrationEffect;
+ }
+ return effect;
+ } catch (IllegalArgumentException iae) {
+ // Deep in the bowels of the VibrationEffect class it is possible for an
+ // IllegalArgumentException to be thrown if there is an invalid URI specified in the
+ // device config, or a content provider failure. Rather than crashing the Telecom
+ // process we will just use the default vibration effect.
+ Log.e(this, iae, "getVibrationEffectForRingtone: failed to get vibration effect");
+ return mDefaultVibrationEffect;
}
- return effect;
}
public void startCallWaiting(Call call) {
@@ -419,7 +551,8 @@ public class Ringer {
return;
}
- if (mInCallController.doesConnectedDialerSupportRinging()) {
+ if (mInCallController.doesConnectedDialerSupportRinging(
+ call.getAssociatedUser())) {
Log.addEvent(call, LogUtils.Events.SKIP_RINGING, "Dialer handles");
return;
}
@@ -443,6 +576,13 @@ public class Ringer {
}
public void stopRinging() {
+ final Call foregroundCall = mRingingCall != null ? mRingingCall : mVibratingCall;
+ if (mAccessibilityManagerAdapter != null) {
+ Log.addEvent(foregroundCall, LogUtils.Events.FLASH_NOTIFICATION_STOP);
+ getHandler().post(() ->
+ mAccessibilityManagerAdapter.stopFlashNotificationSequence(mContext));
+ }
+
synchronized (mLock) {
if (mRingingCall != null) {
Log.addEvent(mRingingCall, LogUtils.Events.STOP_RINGER);
@@ -451,18 +591,12 @@ public class Ringer {
mRingtonePlayer.stop();
- // If we haven't started vibrating because we were waiting for the haptics info, cancel
- // it and don't vibrate at all.
- if (mVibrateFuture != null) {
- mVibrateFuture.cancel(true);
- }
-
if (mIsVibrating) {
Log.addEvent(mVibratingCall, LogUtils.Events.STOP_VIBRATOR);
mVibrator.cancel();
mIsVibrating = false;
- mVibratingCall = null;
}
+ mVibratingCall = null; // Prevents vibrations from starting via AsyncRingtonePlayer.
}
}
@@ -483,16 +617,26 @@ public class Ringer {
return mRingtonePlayer.isPlaying();
}
- private boolean shouldRingForContact(Uri contactUri) {
- final NotificationManager manager =
- (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ /**
+ * shouldRingForContact checks if the caller matches one of the Do Not Disturb bypass
+ * settings (ex. A contact or repeat caller might be able to bypass DND settings). If
+ * matchesCallFilter returns true, this means the caller can bypass the Do Not Disturb settings
+ * and interrupt the user; otherwise call is suppressed.
+ */
+ public boolean shouldRingForContact(Call call) {
+ // avoid re-computing manager.matcherCallFilter(Bundle)
+ if (call.wasDndCheckComputedForCall()) {
+ 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);
}
- return manager.matchesCallFilter(peopleExtras);
+ return mNotificationManager.matchesCallFilter(peopleExtras);
}
private boolean hasExternalRinger(Call foregroundCall) {
@@ -508,10 +652,8 @@ public class Ringer {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
// Use AudioManager#getRingerMode for more accurate result, instead of
// AudioManager#getRingerModeInternal which only useful for volume controllers
- NotificationManager notificationManager = context.getSystemService(
- NotificationManager.class);
- boolean zenModeOn = notificationManager != null
- && notificationManager.getZenMode() != ZEN_MODE_OFF;
+ boolean zenModeOn = mNotificationManager != null
+ && mNotificationManager.getZenMode() != ZEN_MODE_OFF;
return mVibrator.hasVibrator()
&& mSystemSettingsUtil.isRingVibrationEnabled(context)
&& (audioManager.getRingerMode() != AudioManager.RINGER_MODE_SILENT
@@ -526,22 +668,19 @@ public class Ringer {
boolean isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;
timer.record("isVolumeOverZero");
- boolean shouldRingForContact = shouldRingForContact(call.getHandle());
+ boolean shouldRingForContact = shouldRingForContact(call);
timer.record("shouldRingForContact");
- boolean isRingtonePresent = !(mRingtoneFactory.getRingtone(call) == null);
- timer.record("getRingtone");
boolean isSelfManaged = call.isSelfManaged();
timer.record("isSelfManaged");
boolean isSilentRingingRequested = call.isSilentRingingRequested();
timer.record("isSilentRingRequested");
- boolean isRingerAudible = isVolumeOverZero && shouldRingForContact && isRingtonePresent;
+ boolean isRingerAudible = isVolumeOverZero && shouldRingForContact;
timer.record("isRingerAudible");
String inaudibleReason = "";
if (!isRingerAudible) {
- inaudibleReason = String.format(
- "isVolumeOverZero=%s, shouldRingForContact=%s, isRingtonePresent=%s",
- isVolumeOverZero, shouldRingForContact, isRingtonePresent);
+ inaudibleReason = String.format("isVolumeOverZero=%s, shouldRingForContact=%s",
+ isVolumeOverZero, shouldRingForContact);
}
boolean hasExternalRinger = hasExternalRinger(call);
@@ -549,27 +688,32 @@ public class Ringer {
// Don't do call waiting operations or vibration unless these are false.
boolean isTheaterModeOn = mSystemSettingsUtil.isTheaterModeOn(mContext);
timer.record("isTheaterModeOn");
- boolean letDialerHandleRinging = mInCallController.doesConnectedDialerSupportRinging();
+ boolean letDialerHandleRinging = mInCallController.doesConnectedDialerSupportRinging(
+ call.getAssociatedUser());
timer.record("letDialerHandleRinging");
+ boolean isWorkProfileInQuietMode =
+ isProfileInQuietMode(call.getAssociatedUser());
+ timer.record("isWorkProfileInQuietMode");
Log.i(this, "startRinging timings: " + timer);
boolean endEarly = isTheaterModeOn || letDialerHandleRinging || isSelfManaged ||
- hasExternalRinger || isSilentRingingRequested;
+ hasExternalRinger || isSilentRingingRequested || isWorkProfileInQuietMode;
if (endEarly) {
Log.i(this, "Ending early -- isTheaterModeOn=%s, letDialerHandleRinging=%s, " +
- "isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s",
+ "isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s, " +
+ "isWorkProfileInQuietMode=%s",
isTheaterModeOn, letDialerHandleRinging, isSelfManaged, hasExternalRinger,
- isSilentRingingRequested);
+ isSilentRingingRequested, isWorkProfileInQuietMode);
}
// Acquire audio focus under any of the following conditions:
// 1. Should ring for contact and there's an HFP device attached
// 2. Volume is over zero, we should ring for the contact, and there's a audible ringtone
- // present.
+ // present. (This check is deferred until ringer knows the ringtone)
// 3. The call is self-managed.
- boolean shouldAcquireAudioFocus =
- isRingerAudible || (isHfpDeviceAttached && shouldRingForContact) || isSelfManaged;
+ boolean shouldAcquireAudioFocus = !isWorkProfileInQuietMode &&
+ ((isHfpDeviceAttached && shouldRingForContact) || isSelfManaged);
// Set missed reason according to attributes
if (!isVolumeOverZero) {
@@ -587,9 +731,15 @@ public class Ringer {
.setInaudibleReason(inaudibleReason)
.setShouldRingForContact(shouldRingForContact)
.setSilentRingingRequested(isSilentRingingRequested)
+ .setWorkProfileQuietMode(isWorkProfileInQuietMode)
.build();
}
+ private boolean isProfileInQuietMode(UserHandle user) {
+ UserManager um = mContext.getSystemService(UserManager.class);
+ return um.isManagedProfile(user.getIdentifier()) && um.isQuietModeEnabled(user);
+ }
+
private Handler getHandler() {
if (mHandler == null) {
HandlerThread handlerThread = new HandlerThread("Ringer");
diff --git a/src/com/android/server/telecom/RingerAttributes.java b/src/com/android/server/telecom/RingerAttributes.java
index 840d815d1..e0d3e1c08 100644
--- a/src/com/android/server/telecom/RingerAttributes.java
+++ b/src/com/android/server/telecom/RingerAttributes.java
@@ -25,6 +25,7 @@ public class RingerAttributes {
private String mInaudibleReason;
private boolean mShouldRingForContact;
private boolean mSilentRingingRequested;
+ private boolean mWorkProfileQuietMode;
public RingerAttributes.Builder setEndEarly(boolean endEarly) {
mEndEarly = endEarly;
@@ -61,10 +62,15 @@ public class RingerAttributes {
return this;
}
+ public RingerAttributes.Builder setWorkProfileQuietMode(boolean workProfileQuietMode) {
+ mWorkProfileQuietMode = workProfileQuietMode;
+ return this;
+ }
+
public RingerAttributes build() {
return new RingerAttributes(mEndEarly, mLetDialerHandleRinging, mAcquireAudioFocus,
mRingerAudible, mInaudibleReason, mShouldRingForContact,
- mSilentRingingRequested);
+ mSilentRingingRequested, mWorkProfileQuietMode);
}
}
@@ -75,10 +81,12 @@ public class RingerAttributes {
private String mInaudibleReason;
private boolean mShouldRingForContact;
private boolean mSilentRingingRequested;
+ private boolean mWorkProfileQuietMode;
private RingerAttributes(boolean endEarly, boolean letDialerHandleRinging,
boolean acquireAudioFocus, boolean ringerAudible, String inaudibleReason,
- boolean shouldRingForContact, boolean silentRingingRequested) {
+ boolean shouldRingForContact, boolean silentRingingRequested,
+ boolean workProfileQuietMode) {
mEndEarly = endEarly;
mLetDialerHandleRinging = letDialerHandleRinging;
mAcquireAudioFocus = acquireAudioFocus;
@@ -86,6 +94,7 @@ public class RingerAttributes {
mInaudibleReason = inaudibleReason;
mShouldRingForContact = shouldRingForContact;
mSilentRingingRequested = silentRingingRequested;
+ mWorkProfileQuietMode = workProfileQuietMode;
}
public boolean isEndEarly() {
@@ -115,4 +124,8 @@ public class RingerAttributes {
public boolean isSilentRingingRequested() {
return mSilentRingingRequested;
}
+
+ public boolean isWorkProfileInQuietMode() {
+ return mWorkProfileQuietMode;
+ }
}
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 5c469987f..6bcfb4c2b 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -65,26 +65,30 @@ public class RingtoneFactory {
}
public Ringtone getRingtone(Call incomingCall,
- @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
+ // Initializing ringtones on the main thread can deadlock
+ ThreadUtil.checkNotOnMainThread();
+
+ AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(hapticChannelsMuted);
+
// Use the default ringtone of the work profile if the contact is a work profile contact.
+ // or the default ringtone of the receiving user.
Context userContext = isWorkContact(incomingCall) ?
getWorkProfileContextForUser(mCallsManager.getCurrentUserHandle()) :
- getContextForUserHandle(mCallsManager.getCurrentUserHandle());
+ getContextForUserHandle(incomingCall.getAssociatedUser());
Uri ringtoneUri = incomingCall.getRingtone();
Ringtone ringtone = null;
- AudioAttributes audioAttrs = getRingtoneAudioAttributes();
-
- if(ringtoneUri != null && userContext != null) {
+ if (ringtoneUri != null && userContext != null) {
// Ringtone URI is explicitly specified. First, try to create a Ringtone with that.
try {
- ringtone = RingtoneManager.getRingtone(
- userContext, ringtoneUri, volumeShaperConfig, audioAttrs);
- } catch (NullPointerException npe) {
- Log.e(this, npe, "getRingtone: NPE while getting ringtone.");
+ ringtone = RingtoneManager.getRingtone(
+ userContext, ringtoneUri, volumeShaperConfig, audioAttrs);
+ } catch (Exception e) {
+ Log.e(this, e, "getRingtone: exception while getting ringtone.");
}
}
- if(ringtone == null) {
+ if (ringtone == null) {
// Contact didn't specify ringtone or custom Ringtone creation failed. Get default
// ringtone for user or profile.
Context contextToUse = hasDefaultRingtoneForUser(userContext) ? userContext : mContext;
@@ -101,37 +105,40 @@ public class RingtoneFactory {
Log.i(this, "getRingtone: Settings.System.DEFAULT_RINGTONE_URI is null.");
}
}
+
if (defaultRingtoneUri == null) {
return null;
}
+
try {
ringtone = RingtoneManager.getRingtone(
- contextToUse, defaultRingtoneUri, volumeShaperConfig, audioAttrs);
- } catch (NullPointerException npe) {
- Log.e(this, npe, "getRingtone: NPE while getting ringtone.");
+ contextToUse, defaultRingtoneUri, volumeShaperConfig, audioAttrs);
+ } catch (Exception e) {
+ Log.e(this, e, "getRingtone: exception while getting ringtone.");
}
}
return ringtone;
}
- public AudioAttributes getRingtoneAudioAttributes() {
+ private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
return new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setHapticChannelsMuted(hapticChannelsMuted)
.build();
}
- public Ringtone getRingtone(Call incomingCall) {
- return getRingtone(incomingCall, null);
- }
-
/** 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 = getRingtoneAudioAttributes();
- Ringtone ringtone = RingtoneManager.getRingtone(mContext, ringtoneUri, null, audioAttrs);
+ 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);
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index ba82a0699..8fdfb1189 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -41,7 +41,7 @@ public interface RoleManagerAdapter {
* redirection role.
* @return the package name of the app filling the role, {@code null} otherwise}.
*/
- String getDefaultCallRedirectionApp();
+ String getDefaultCallRedirectionApp(UserHandle userHandle);
/**
* Override the {@link android.app.role.RoleManager} call redirection app with another value.
@@ -56,7 +56,7 @@ public interface RoleManagerAdapter {
* screening role.
* @return the package name of the app filling the role, {@code null} otherwise}.
*/
- String getDefaultCallScreeningApp();
+ String getDefaultCallScreeningApp(UserHandle userHandle);
/**
* Override the {@link android.app.role.RoleManager} call screening app with another value.
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index 4a98d7b90..ac35b3d97 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -20,6 +20,7 @@ import android.app.role.RoleManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.os.Binder;
import android.os.UserHandle;
import android.telecom.Log;
@@ -50,11 +51,11 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {
}
@Override
- public String getDefaultCallRedirectionApp() {
+ public String getDefaultCallRedirectionApp(UserHandle userHandleForCallRedirection) {
if (mOverrideDefaultCallRedirectionApp != null) {
return mOverrideDefaultCallRedirectionApp;
}
- return getRoleManagerCallRedirectionApp();
+ return getRoleManagerCallRedirectionApp(userHandleForCallRedirection);
}
@Override
@@ -63,11 +64,11 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {
}
@Override
- public String getDefaultCallScreeningApp() {
+ public String getDefaultCallScreeningApp(UserHandle userHandleForCallScreening) {
if (mOverrideDefaultCallScreeningApp != null) {
return mOverrideDefaultCallScreeningApp;
}
- return getRoleManagerCallScreeningApp();
+ return getRoleManagerCallScreeningApp(userHandleForCallScreening);
}
@Override
@@ -118,9 +119,9 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {
mCurrentUserHandle = currentUserHandle;
}
- private String getRoleManagerCallScreeningApp() {
+ private String getRoleManagerCallScreeningApp(UserHandle userHandle) {
List<String> roleHolders = mRoleManager.getRoleHoldersAsUser(ROLE_CALL_SCREENING,
- mCurrentUserHandle);
+ userHandle);
if (roleHolders == null || roleHolders.isEmpty()) {
return null;
}
@@ -141,9 +142,9 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {
return new ArrayList<>();
}
- private String getRoleManagerCallRedirectionApp() {
+ private String getRoleManagerCallRedirectionApp(UserHandle userHandle) {
List<String> roleHolders = mRoleManager.getRoleHoldersAsUser(ROLE_CALL_REDIRECTION_APP,
- mCurrentUserHandle);
+ userHandle);
if (roleHolders == null || roleHolders.isEmpty()) {
return null;
}
@@ -184,7 +185,7 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {
pw.print("(override ");
pw.print(mOverrideDefaultCallRedirectionApp);
pw.print(") ");
- pw.print(getRoleManagerCallRedirectionApp());
+ pw.print(getRoleManagerCallRedirectionApp(Binder.getCallingUserHandle()));
}
pw.println();
@@ -193,7 +194,7 @@ public class RoleManagerAdapterImpl implements RoleManagerAdapter {
pw.print("(override ");
pw.print(mOverrideDefaultCallScreeningApp);
pw.print(") ");
- pw.print(getRoleManagerCallScreeningApp());
+ pw.print(getRoleManagerCallScreeningApp(Binder.getCallingUserHandle()));
}
pw.println();
diff --git a/src/com/android/server/telecom/StatusBarNotifier.java b/src/com/android/server/telecom/StatusBarNotifier.java
index af3493e99..772335ed3 100644
--- a/src/com/android/server/telecom/StatusBarNotifier.java
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -29,7 +29,7 @@ import com.android.internal.annotations.VisibleForTesting;
*/
@VisibleForTesting
public class StatusBarNotifier extends CallsManagerListenerBase {
- private static final String SLOT_MUTE = "mute";
+ private static final String SLOT_MUTE = "telecom_mute";
private static final String SLOT_SPEAKERPHONE = "speakerphone";
private final Context mContext;
@@ -79,13 +79,15 @@ public class StatusBarNotifier extends CallsManagerListenerBase {
mIsShowingMute = isMuted;
}
+ /**
+ * Update the status bar manager with the new speakerphone state.
+ *
+ * IMPORTANT: DO NOT call into any Telecom code here; this is usually scheduled on an async
+ * executor to save Telecom from blocking on outgoing binder calls.
+ * @param isSpeakerphone
+ */
@VisibleForTesting
public void notifySpeakerphone(boolean isSpeakerphone) {
- // Never display anything if there are no calls.
- if (!mCallsManager.hasAnyCalls()) {
- isSpeakerphone = false;
- }
-
if (mIsShowingSpeakerphone == isSpeakerphone) {
return;
}
diff --git a/src/com/android/server/telecom/StreamingCallAdapter.java b/src/com/android/server/telecom/StreamingCallAdapter.java
new file mode 100644
index 000000000..e899aff4e
--- /dev/null
+++ b/src/com/android/server/telecom/StreamingCallAdapter.java
@@ -0,0 +1,72 @@
+/*
+ * 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 android.os.Binder;
+import android.os.RemoteException;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telecom.StreamingCall;
+
+import com.android.internal.telecom.IStreamingCallAdapter;
+
+/**
+ * Receives call commands and updates from general call streaming app and passes them through to
+ * the original voip call app. {@link android.telecom.CallStreamingService} creates an instance of
+ * this class and passes it to the general call streaming app after binding to it. This adapter can
+ * receive commands and updates until the general call streaming app is unbound.
+ */
+public class StreamingCallAdapter extends IStreamingCallAdapter.Stub {
+ private final static String TAG = "StreamingCallAdapter";
+
+ private final TransactionalServiceWrapper mTransactionalServiceWrapper;
+ private final Call mCall;
+ private final String mOwnerPackageAbbreviation;
+
+ public StreamingCallAdapter(TransactionalServiceWrapper wrapper, Call call,
+ String ownerPackageName) {
+ mTransactionalServiceWrapper = wrapper;
+ mCall = call;
+ mOwnerPackageAbbreviation = Log.getPackageAbbreviation(ownerPackageName);
+ }
+
+ @Override
+ public void setStreamingState(int state) throws RemoteException {
+ try {
+ Log.startSession(LogUtils.Sessions.CSA_SET_STATE, mOwnerPackageAbbreviation);
+ long token = Binder.clearCallingIdentity();
+ try {
+ Log.i(this, "setStreamingState(%d)", state);
+ switch (state) {
+ case StreamingCall.STATE_STREAMING:
+ mTransactionalServiceWrapper.onSetActive(mCall);
+ case StreamingCall.STATE_HOLDING:
+ mTransactionalServiceWrapper.onSetInactive(mCall);
+ case StreamingCall.STATE_DISCONNECTED:
+ mTransactionalServiceWrapper.onDisconnect(mCall,
+ new DisconnectCause(DisconnectCause.LOCAL));
+ default:
+ // ignore
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/SystemStateHelper.java b/src/com/android/server/telecom/SystemStateHelper.java
index dd978c23b..5eed1acd7 100644
--- a/src/com/android/server/telecom/SystemStateHelper.java
+++ b/src/com/android/server/telecom/SystemStateHelper.java
@@ -146,9 +146,11 @@ public class SystemStateHelper implements UiModeManager.OnProjectionStateChanged
IntentFilter intentFilter1 = new IntentFilter(
UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED);
intentFilter1.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED);
+ intentFilter1.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
IntentFilter intentFilter2 = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
intentFilter2.addDataScheme("package");
+ intentFilter2.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiver(mBroadcastReceiver, intentFilter1);
mContext.registerReceiver(mBroadcastReceiver, intentFilter2);
Log.i(this, "Registering broadcast receiver: %s", intentFilter1);
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index e1f2d0889..523b8418e 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -16,10 +16,12 @@
package com.android.server.telecom;
+import android.app.BroadcastOptions;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.os.Bundle;
import android.os.UserHandle;
import android.telecom.Log;
import android.widget.Toast;
@@ -99,6 +101,10 @@ public final class TelecomBroadcastIntentProcessor {
public static final String ACTION_CANCEL_REDIRECTED_CALL =
"com.android.server.telecom.CANCEL_REDIRECTED_CALL";
+ public static final String ACTION_HANGUP_CALL = "com.android.server.telecom.HANGUP_CALL";
+ public static final String ACTION_STOP_STREAMING =
+ "com.android.server.telecom.ACTION_STOP_STREAMING";
+
public static final String EXTRA_USERHANDLE = "userhandle";
public static final String EXTRA_REDIRECTION_OUTGOING_CALL_ID =
"android.telecom.extra.REDIRECTION_OUTGOING_CALL_ID";
@@ -240,6 +246,26 @@ public final class TelecomBroadcastIntentProcessor {
} finally {
Log.endSession();
}
+ } else if (ACTION_HANGUP_CALL.equals(action)) {
+ Log.startSession("TBIP.aHC", "streamingDialog");
+ try {
+ Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+ if (call != null) {
+ mCallsManager.disconnectCall(call);
+ }
+ } finally {
+ Log.endSession();
+ }
+ } else if (ACTION_STOP_STREAMING.equals(action)) {
+ Log.startSession("TBIP.aSS", "streamingDialog");
+ try {
+ Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+ if (call != null) {
+ mCallsManager.stopCallStreaming(call);
+ }
+ } finally {
+ Log.endSession();
+ }
}
}
@@ -247,8 +273,14 @@ public final class TelecomBroadcastIntentProcessor {
* Closes open system dialogs and the notification shade.
*/
private void closeSystemDialogs(Context context) {
- Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
- context.sendBroadcastAsUser(intent, UserHandle.ALL);
+ Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ Bundle options = BroadcastOptions.makeBasic()
+ .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+ .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
+ .toBundle();
+ context.sendBroadcastAsUser(intent, UserHandle.ALL, null /* receiverPermission */,
+ options);
}
private void sendSmsIntent(Intent intent, UserHandle userHandle) {
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index ee7aba6c7..f33b18586 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -19,6 +19,7 @@ package com.android.server.telecom;
import static android.Manifest.permission.CALL_PHONE;
import static android.Manifest.permission.CALL_PRIVILEGED;
import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.MANAGE_OWN_CALLS;
import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.Manifest.permission.READ_PHONE_NUMBERS;
import static android.Manifest.permission.READ_PHONE_STATE;
@@ -26,7 +27,10 @@ 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 android.Manifest.permission.MANAGE_OWN_CALLS;
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
+import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
import android.Manifest;
import android.app.ActivityManager;
@@ -43,14 +47,19 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.OutcomeReceiver;
import android.os.Process;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.BlockedNumberContract;
import android.provider.Settings;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -62,15 +71,28 @@ import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.EventLog;
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telecom.ICallControl;
+import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
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 java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
// TODO: Needed for move to system service: import com.android.internal.R;
@@ -109,12 +131,149 @@ public class TelecomServiceImpl {
}
}
+ 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.
+ */
+ public static final UUID REGISTER_PHONE_ACCOUNT_ERROR_UUID =
+ UUID.fromString("0e49f82e-6acc-48a9-b088-66c8296c1eb5");
+ public static final String REGISTER_PHONE_ACCOUNT_ERROR_MSG =
+ "Exception thrown while registering phone account.";
+ public static final UUID SET_USER_PHONE_ACCOUNT_ERROR_UUID =
+ UUID.fromString("80866066-7818-4869-bd44-1f7f689543e2");
+ public static final String SET_USER_PHONE_ACCOUNT_ERROR_MSG =
+ "Exception thrown while setting the user selected outgoing phone account.";
+ public static final UUID GET_CALL_CAPABLE_ACCOUNTS_ERROR_UUID =
+ UUID.fromString("4f39b865-01f2-4c1f-83a5-37ce52807e83");
+ public static final String GET_CALL_CAPABLE_ACCOUNTS_ERROR_MSG =
+ "Exception thrown while getting the call capable phone accounts";
+ public static final UUID GET_PHONE_ACCOUNT_ERROR_UUID =
+ UUID.fromString("b653c1f0-91b4-45c8-ad05-3ee4d1006c7f");
+ public static final String GET_PHONE_ACCOUNT_ERROR_MSG =
+ "Exception thrown while retrieving the phone account.";
+ public static final UUID GET_SIM_MANAGER_ERROR_UUID =
+ UUID.fromString("4244cb3f-bd02-4cc5-9f90-f41ea62ce0bb");
+ public static final String GET_SIM_MANAGER_ERROR_MSG =
+ "Exception thrown while retrieving the SIM CallManager.";
+ public static final UUID GET_SIM_MANAGER_FOR_USER_ERROR_UUID =
+ UUID.fromString("5d347ce7-7527-40d3-b98a-09b423ad031c");
+ public static final String GET_SIM_MANAGER_FOR_USER_ERROR_MSG =
+ "Exception thrown while retrieving the SIM CallManager based on the provided user.";
+ public static final UUID PLACE_CALL_SECURITY_EXCEPTION_ERROR_UUID =
+ 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 final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {
+
+ @Override
+ public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
+ String callId, String callingPackage) {
+ try {
+ Log.startSession("TSI.aC", Log.getPackageAbbreviation(callingPackage));
+ Log.i(TAG, "addCall: id=[%s], attributes=[%s]", callId, callAttributes);
+ PhoneAccountHandle handle = callAttributes.getPhoneAccountHandle();
+
+ // enforce permissions and arguments
+ enforcePermission(android.Manifest.permission.MANAGE_OWN_CALLS);
+ enforceUserHandleMatchesCaller(handle);
+ enforcePhoneAccountIsNotManaged(handle);// only allow self-managed packages (temp.)
+ enforcePhoneAccountIsRegisteredEnabled(handle, handle.getUserHandle());
+ enforceCallingPackage(callingPackage, "addCall");
+
+ // 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;
+ // create transaction based on the call direction
+ switch (callAttributes.getDirection()) {
+ case DIRECTION_OUTGOING:
+ transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
+ mCallsManager, extras);
+ break;
+ case DIRECTION_INCOMING:
+ transaction = new IncomingCallTransaction(callId, callAttributes,
+ mCallsManager, extras);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Invalid Call Direction. "
+ + "Was [%d] but should be within [%d,%d]",
+ callAttributes.getDirection(), DIRECTION_INCOMING,
+ DIRECTION_OUTGOING));
+ }
+
+ mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ Log.d(TAG, "addCall: onResult");
+ Call call = result.getCall();
+
+ if (call == null || !call.getId().equals(callId)) {
+ Log.i(TAG, "addCall: onResult: call is null or id mismatch");
+ onAddCallControl(callId, callEventCallback, null,
+ new CallException(ADD_CALL_ERR_MSG, CODE_ERROR_UNKNOWN));
+ return;
+ }
+
+ TransactionalServiceWrapper serviceWrapper =
+ mTransactionalServiceRepository
+ .addNewCallForTransactionalServiceWrapper(handle,
+ callEventCallback, mCallsManager, call);
+
+ call.setTransactionServiceWrapper(serviceWrapper);
+ ICallControl clientCallControl = serviceWrapper.getICallControl();
+
+ if (clientCallControl == null) {
+ throw new IllegalStateException("TransactionalServiceWrapper"
+ + "#ICallControl is null.");
+ }
+
+ // finally, send objects back to the client
+ onAddCallControl(callId, callEventCallback, clientCallControl, null);
+ }
+
+ @Override
+ public void onError(@NonNull CallException exception) {
+ Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
+ onAddCallControl(callId, callEventCallback, null, exception);
+ }
+ });
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ private void onAddCallControl(String callId, ICallEventCallback callEventCallback,
+ ICallControl callControl, CallException callException) {
+ try {
+ if (callException == null) {
+ callEventCallback.onAddCallControl(callId, TELECOM_TRANSACTION_SUCCESS,
+ callControl, null);
+ } else {
+ callEventCallback.onAddCallControl(callId,
+ CallException.CODE_ERROR_UNKNOWN,
+ null, callException);
+ }
+ } catch (RemoteException remoteException) {
+ throw remoteException.rethrowAsRuntimeException();
+ }
+ }
+
@Override
public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme,
String callingPackage, String callingFeatureId) {
@@ -134,11 +293,11 @@ public class TelecomServiceImpl {
Binder.restoreCallingIdentity(token);
}
if (isCallerSimCallManager(phoneAccountHandle)
- || canReadPhoneState(
+ || canReadPhoneState(
callingPackage,
callingFeatureId,
"getDefaultOutgoingPhoneAccount")) {
- return phoneAccountHandle;
+ return phoneAccountHandle;
}
return null;
}
@@ -181,6 +340,8 @@ public class TelecomServiceImpl {
accountHandle, callingUserHandle);
} catch (Exception e) {
Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
+ mAnomalyReporter.reportAnomaly(SET_USER_PHONE_ACCOUNT_ERROR_UUID,
+ SET_USER_PHONE_ACCOUNT_ERROR_MSG);
throw e;
} finally {
Binder.restoreCallingIdentity(token);
@@ -207,13 +368,17 @@ public class TelecomServiceImpl {
}
synchronized (mLock) {
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ boolean crossUserAccess = hasInAppCrossUserPermission();
long token = Binder.clearCallingIdentity();
try {
return new ParceledListSlice<>(
mPhoneAccountRegistrar.getCallCapablePhoneAccounts(null,
- includeDisabledAccounts, callingUserHandle));
+ includeDisabledAccounts, callingUserHandle,
+ crossUserAccess));
} catch (Exception e) {
Log.e(this, e, "getCallCapablePhoneAccounts");
+ mAnomalyReporter.reportAnomaly(GET_CALL_CAPABLE_ACCOUNTS_ERROR_UUID,
+ GET_CALL_CAPABLE_ACCOUNTS_ERROR_MSG);
throw e;
} finally {
Binder.restoreCallingIdentity(token);
@@ -258,8 +423,7 @@ public class TelecomServiceImpl {
Log.startSession("TSI.gOSMPA", Log.getPackageAbbreviation(callingPackage));
try {
enforceCallingPackage(callingPackage, "getOwnSelfManagedPhoneAccounts");
- }
- catch(SecurityException se){
+ } catch (SecurityException se) {
EventLog.writeEvent(0x534e4554, "231986341", Binder.getCallingUid(),
"getOwnSelfManagedPhoneAccounts: invalid calling package");
throw se;
@@ -273,7 +437,7 @@ public class TelecomServiceImpl {
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
.getSelfManagedPhoneAccountsForPackage(callingPackage,
- callingUserHandle));
+ callingUserHandle));
} catch (Exception e) {
Log.e(this, e,
"getSelfManagedPhoneAccountsForPackage");
@@ -306,8 +470,8 @@ public class TelecomServiceImpl {
long token = Binder.clearCallingIdentity();
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
- .getCallCapablePhoneAccounts(uriScheme, false,
- callingUserHandle));
+ .getCallCapablePhoneAccounts(uriScheme, false,
+ callingUserHandle, false));
} catch (Exception e) {
Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme);
throw e;
@@ -346,7 +510,7 @@ public class TelecomServiceImpl {
try {
Log.startSession("TSI.gPAFP");
return new ParceledListSlice<>(mPhoneAccountRegistrar
- .getPhoneAccountsForPackage(packageName, callingUserHandle));
+ .getAllPhoneAccountHandlesForPackage(callingUserHandle, packageName));
} catch (Exception e) {
Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
throw e;
@@ -361,42 +525,51 @@ public class TelecomServiceImpl {
public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle,
String callingPackage) {
try {
- enforceCallingPackage(callingPackage, "getPhoneAccount");
- } catch (SecurityException se) {
- EventLog.writeEvent(0x534e4554, "196406138", Binder.getCallingUid(),
- "getPhoneAccount: invalid calling package");
- throw se;
- }
- synchronized (mLock) {
- final UserHandle callingUserHandle = Binder.getCallingUserHandle();
- if (CompatChanges.isChangeEnabled(
- TelecomManager.ENABLE_GET_PHONE_ACCOUNT_PERMISSION_PROTECTION,
- callingPackage, Binder.getCallingUserHandle())) {
- if (Binder.getCallingUid() != Process.SHELL_UID &&
- !canGetPhoneAccount(callingPackage, accountHandle)) {
- SecurityException e = new SecurityException("getPhoneAccount API requires" +
- "READ_PHONE_NUMBERS");
+ Log.startSession("TSI.gPA", Log.getPackageAbbreviation(callingPackage));
+ try {
+ enforceCallingPackage(callingPackage, "getPhoneAccount");
+ } catch (SecurityException se) {
+ EventLog.writeEvent(0x534e4554, "196406138", Binder.getCallingUid(),
+ "getPhoneAccount: invalid calling package");
+ throw se;
+ }
+ synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ if (CompatChanges.isChangeEnabled(
+ TelecomManager.ENABLE_GET_PHONE_ACCOUNT_PERMISSION_PROTECTION,
+ callingPackage, Binder.getCallingUserHandle())) {
+ if (Binder.getCallingUid() != Process.SHELL_UID &&
+ !canGetPhoneAccount(callingPackage, accountHandle)) {
+ SecurityException e = new SecurityException(
+ "getPhoneAccount API requires" +
+ "READ_PHONE_NUMBERS");
+ Log.e(this, e, "getPhoneAccount %s", accountHandle);
+ throw e;
+ }
+ }
+ Set<String> permissions = computePermissionsForBoundPackage(
+ Set.of(MODIFY_PHONE_STATE), null);
+ long token = Binder.clearCallingIdentity();
+ 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
+ // in-call UI, parent user's in call UI need to be able to get phone account
+ // from the profile's phone account handle.
+ PhoneAccount account = mPhoneAccountRegistrar
+ .getPhoneAccount(accountHandle, callingUserHandle,
+ /* acrossProfiles */ true);
+ return maybeCleansePhoneAccount(account, permissions);
+ } catch (Exception e) {
Log.e(this, e, "getPhoneAccount %s", accountHandle);
+ mAnomalyReporter.reportAnomaly(GET_PHONE_ACCOUNT_ERROR_UUID,
+ GET_PHONE_ACCOUNT_ERROR_MSG);
throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
}
- long token = Binder.clearCallingIdentity();
- try {
- Log.startSession("TSI.gPA");
- // 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 in-call UI,
- // parent user's in call UI need to be able to get phone account from the
- // profile's phone account handle.
- return mPhoneAccountRegistrar
- .getPhoneAccount(accountHandle, callingUserHandle,
- /* acrossProfiles */ true);
- } catch (Exception e) {
- Log.e(this, e, "getPhoneAccount %s", accountHandle);
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
- Log.endSession();
- }
+ } finally {
+ Log.endSession();
}
}
@@ -446,7 +619,7 @@ public class TelecomServiceImpl {
long token = Binder.clearCallingIdentity();
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
- .getAllPhoneAccounts(callingUserHandle));
+ .getAllPhoneAccounts(callingUserHandle, false));
} catch (Exception e) {
Log.e(this, e, "getAllPhoneAccounts");
throw e;
@@ -474,10 +647,12 @@ public class TelecomServiceImpl {
synchronized (mLock) {
final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ boolean crossUserAccess = hasInAppCrossUserPermission();
long token = Binder.clearCallingIdentity();
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
- .getAllPhoneAccountHandles(callingUserHandle));
+ .getAllPhoneAccountHandles(callingUserHandle,
+ crossUserAccess));
} catch (Exception e) {
Log.e(this, e, "getAllPhoneAccounts");
throw e;
@@ -491,10 +666,10 @@ public class TelecomServiceImpl {
}
@Override
- public PhoneAccountHandle getSimCallManager(int subId) {
+ public PhoneAccountHandle getSimCallManager(int subId, String callingPackage) {
synchronized (mLock) {
try {
- Log.startSession("TSI.gSCM");
+ Log.startSession("TSI.gSCM", Log.getPackageAbbreviation(callingPackage));
final int callingUid = Binder.getCallingUid();
final int user = UserHandle.getUserId(callingUid);
long token = Binder.clearCallingIdentity();
@@ -508,6 +683,8 @@ public class TelecomServiceImpl {
}
} catch (Exception e) {
Log.e(this, e, "getSimCallManager");
+ mAnomalyReporter.reportAnomaly(GET_SIM_MANAGER_ERROR_UUID,
+ GET_SIM_MANAGER_ERROR_MSG);
throw e;
} finally {
Log.endSession();
@@ -516,10 +693,10 @@ public class TelecomServiceImpl {
}
@Override
- public PhoneAccountHandle getSimCallManagerForUser(int user) {
+ public PhoneAccountHandle getSimCallManagerForUser(int user, String callingPackage) {
synchronized (mLock) {
try {
- Log.startSession("TSI.gSCMFU");
+ Log.startSession("TSI.gSCMFU", Log.getPackageAbbreviation(callingPackage));
final int callingUid = Binder.getCallingUid();
if (user != ActivityManager.getCurrentUser()) {
enforceCrossUserPermission(callingUid);
@@ -532,6 +709,8 @@ public class TelecomServiceImpl {
}
} catch (Exception e) {
Log.e(this, e, "getSimCallManager");
+ mAnomalyReporter.reportAnomaly(GET_SIM_MANAGER_FOR_USER_ERROR_UUID,
+ GET_SIM_MANAGER_FOR_USER_ERROR_MSG);
throw e;
} finally {
Log.endSession();
@@ -540,9 +719,9 @@ public class TelecomServiceImpl {
}
@Override
- public void registerPhoneAccount(PhoneAccount account) {
+ public void registerPhoneAccount(PhoneAccount account, String callingPackage) {
try {
- Log.startSession("TSI.rPA");
+ Log.startSession("TSI.rPA", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
try {
enforcePhoneAccountModificationForPackage(
@@ -573,9 +752,9 @@ public class TelecomServiceImpl {
// and carrier-designated SIM call manager can register accounts with these
// capabilities.
if (account.hasCapabilities(
- PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS)
+ PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS)
|| account.hasCapabilities(
- PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE)) {
+ PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE)) {
enforceRegisterVoiceCallingIndicationCapabilities(account);
}
Bundle extras = account.getExtras();
@@ -599,14 +778,21 @@ public class TelecomServiceImpl {
.build();
}
+ // Validate the profile boundary of the given image URI.
+ validateAccountIconUserBoundary(account.getIcon());
+
final long token = Binder.clearCallingIdentity();
try {
+ Log.i(this, "registerPhoneAccount: account=%s",
+ account);
mPhoneAccountRegistrar.registerPhoneAccount(account);
} finally {
Binder.restoreCallingIdentity(token);
}
} catch (Exception e) {
Log.e(this, e, "registerPhoneAccount %s", account);
+ mAnomalyReporter.reportAnomaly(REGISTER_PHONE_ACCOUNT_ERROR_UUID,
+ REGISTER_PHONE_ACCOUNT_ERROR_MSG);
throw e;
}
}
@@ -616,10 +802,11 @@ public class TelecomServiceImpl {
}
@Override
- public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
+ public void unregisterPhoneAccount(PhoneAccountHandle accountHandle,
+ String callingPackage) {
synchronized (mLock) {
try {
- Log.startSession("TSI.uPA");
+ Log.startSession("TSI.uPA", Log.getPackageAbbreviation(callingPackage));
enforcePhoneAccountModificationForPackage(
accountHandle.getComponentName().getPackageName());
enforceUserHandleMatchesCaller(accountHandle);
@@ -662,7 +849,7 @@ public class TelecomServiceImpl {
public boolean isVoiceMailNumber(PhoneAccountHandle accountHandle, String number,
String callingPackage, String callingFeatureId) {
try {
- Log.startSession("TSI.iVMN");
+ Log.startSession("TSI.iVMN", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
if (!canReadPhoneState(callingPackage, callingFeatureId, "isVoiceMailNumber")) {
return false;
@@ -695,7 +882,7 @@ public class TelecomServiceImpl {
public String getVoiceMailNumber(PhoneAccountHandle accountHandle, String callingPackage,
String callingFeatureId) {
try {
- Log.startSession("TSI.gVMN");
+ Log.startSession("TSI.gVMN", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "getVoiceMailNumber")) {
return null;
}
@@ -731,7 +918,7 @@ public class TelecomServiceImpl {
public String getLine1Number(PhoneAccountHandle accountHandle, String callingPackage,
String callingFeatureId) {
try {
- Log.startSession("getL1N");
+ Log.startSession("getL1N", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneNumbers(callingPackage, callingFeatureId, "getLine1Number")) {
return null;
}
@@ -768,15 +955,18 @@ public class TelecomServiceImpl {
@Override
public void silenceRinger(String callingPackage) {
try {
- Log.startSession("TSI.sR");
+ Log.startSession("TSI.sR", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
-
+ UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ boolean crossUserAccess = hasInAppCrossUserPermission();
long token = Binder.clearCallingIdentity();
try {
Log.i(this, "Silence Ringer requested by %s", callingPackage);
- mCallsManager.getCallAudioManager().silenceRingers();
- mCallsManager.getInCallController().silenceRinger();
+ Set<UserHandle> userHandles = mCallsManager.getCallAudioManager().
+ silenceRingers(mContext, callingUserHandle,
+ crossUserAccess);
+ mCallsManager.getInCallController().silenceRinger(userHandles);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -789,7 +979,7 @@ public class TelecomServiceImpl {
/**
* @see android.telecom.TelecomManager#getDefaultPhoneApp
* @deprecated - Use {@link android.telecom.TelecomManager#getDefaultDialerPackage()}
- * instead.
+ * instead.
*/
@Override
public ComponentName getDefaultPhoneApp() {
@@ -803,18 +993,19 @@ public class TelecomServiceImpl {
/**
* @return the package name of the current user-selected default dialer. If no default
- * has been selected, the package name of the system dialer is returned. If
- * neither exists, then {@code null} is returned.
+ * has been selected, the package name of the system dialer is returned. If
+ * neither exists, then {@code null} is returned.
* @see android.telecom.TelecomManager#getDefaultDialerPackage
*/
@Override
- public String getDefaultDialerPackage() {
+ public String getDefaultDialerPackage(String callingPackage) {
try {
- Log.startSession("TSI.gDDP");
+ Log.startSession("TSI.gDDP", Log.getPackageAbbreviation(callingPackage));
+ int callerUserId = UserHandle.getCallingUserId();
final long token = Binder.clearCallingIdentity();
try {
return mDefaultDialerCache.getDefaultDialerApplication(
- ActivityManager.getCurrentUser());
+ callerUserId);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -826,8 +1017,8 @@ public class TelecomServiceImpl {
/**
* @param userId user id to get the default dialer package for
* @return the package name of the current user-selected default dialer. If no default
- * has been selected, the package name of the system dialer is returned. If
- * neither exists, then {@code null} is returned.
+ * has been selected, the package name of the system dialer is returned. If
+ * neither exists, then {@code null} is returned.
* @see android.telecom.TelecomManager#getDefaultDialerPackage
*/
@Override
@@ -852,9 +1043,9 @@ public class TelecomServiceImpl {
* @see android.telecom.TelecomManager#getSystemDialerPackage
*/
@Override
- public String getSystemDialerPackage() {
+ public String getSystemDialerPackage(String callingPackage) {
try {
- Log.startSession("TSI.gSDP");
+ Log.startSession("TSI.gSDP", Log.getPackageAbbreviation(callingPackage));
return mDefaultDialerCache.getSystemDialerApplication();
} finally {
Log.endSession();
@@ -885,13 +1076,14 @@ public class TelecomServiceImpl {
@Override
public boolean isInCall(String callingPackage, String callingFeatureId) {
try {
- Log.startSession("TSI.iIC");
+ Log.startSession("TSI.iIC", Log.getPackageAbbreviation(callingPackage));
if (!canReadPhoneState(callingPackage, callingFeatureId, "isInCall")) {
return false;
}
synchronized (mLock) {
- return mCallsManager.hasOngoingCalls();
+ return mCallsManager.hasOngoingCalls(Binder.getCallingUserHandle(),
+ hasInAppCrossUserPermission());
}
} finally {
Log.endSession();
@@ -904,7 +1096,7 @@ public class TelecomServiceImpl {
@Override
public boolean hasManageOngoingCallsPermission(String callingPackage) {
try {
- Log.startSession("TSI.hMOCP");
+ Log.startSession("TSI.hMOCP", Log.getPackageAbbreviation(callingPackage));
enforceCallingPackage(callingPackage, "hasManageOngoingCallsPermission");
return PermissionChecker.checkPermissionForDataDeliveryFromDataSource(
mContext, Manifest.permission.MANAGE_ONGOING_CALLS,
@@ -913,7 +1105,7 @@ public class TelecomServiceImpl {
new AttributionSource(Binder.getCallingUid(),
callingPackage, /*attributionTag*/ null)),
"Checking whether the caller has MANAGE_ONGOING_CALLS permission")
- == PermissionChecker.PERMISSION_GRANTED;
+ == PermissionChecker.PERMISSION_GRANTED;
} finally {
Log.endSession();
}
@@ -925,14 +1117,15 @@ public class TelecomServiceImpl {
@Override
public boolean isInManagedCall(String callingPackage, String callingFeatureId) {
try {
- Log.startSession("TSI.iIMC");
+ 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.");
}
synchronized (mLock) {
- return mCallsManager.hasOngoingManagedCalls();
+ return mCallsManager.hasOngoingManagedCalls(Binder.getCallingUserHandle(),
+ hasInAppCrossUserPermission());
}
} finally {
Log.endSession();
@@ -1002,6 +1195,22 @@ public class TelecomServiceImpl {
public int getCallStateUsingPackage(String callingPackage, String callingFeatureId) {
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)
+ && !callingUidMatchesPackageManagerRecords(callingPackage)) {
+ EventLog.writeEvent(0x534e4554, "236813210", Binder.getCallingUid(),
+ "getCallStateUsingPackage");
+ Log.i(this,
+ "getCallStateUsingPackage: packageName does not match records for "
+ + "callingPackage=[%s], callingUid=[%d]",
+ callingPackage, Binder.getCallingUid());
+ throw new SecurityException(String.format("getCallStateUsingPackage: "
+ + "enforceCallingPackage: callingPackage=[%s], callingUid=[%d]",
+ callingPackage, Binder.getCallingUid()));
+ }
+
if (CompatChanges.isChangeEnabled(
TelecomManager.ENABLE_GET_CALL_STATE_PERMISSION_PROTECTION, callingPackage,
Binder.getCallingUserHandle())) {
@@ -1020,6 +1229,19 @@ public class TelecomServiceImpl {
}
}
+ private boolean isPrivilegedUid(String callingPackage) {
+ 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 isPrivileged;
+ }
+
/**
* @see android.telecom.TelecomManager#endCall
*/
@@ -1068,7 +1290,6 @@ public class TelecomServiceImpl {
/**
* @see android.telecom.TelecomManager#acceptRingingCall(int)
- *
*/
@Override
public void acceptRingingCallWithVideoState(String packageName, int videoState) {
@@ -1103,9 +1324,10 @@ public class TelecomServiceImpl {
synchronized (mLock) {
+ UserHandle callingUser = Binder.getCallingUserHandle();
long token = Binder.clearCallingIdentity();
try {
- mCallsManager.getInCallController().bringToForeground(showDialpad);
+ mCallsManager.getInCallController().bringToForeground(showDialpad, callingUser);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1136,6 +1358,7 @@ public class TelecomServiceImpl {
Log.endSession();
}
}
+
/**
* @see android.telecom.TelecomManager#handleMmi
*/
@@ -1158,7 +1381,7 @@ public class TelecomServiceImpl {
}
return retval;
- }finally {
+ } finally {
Log.endSession();
}
}
@@ -1199,7 +1422,7 @@ public class TelecomServiceImpl {
Binder.restoreCallingIdentity(token);
}
return retval;
- }finally {
+ } finally {
Log.endSession();
}
}
@@ -1282,9 +1505,10 @@ public class TelecomServiceImpl {
* @see android.telecom.TelecomManager#addNewIncomingCall
*/
@Override
- public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+ public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras,
+ String callingPackage) {
try {
- Log.startSession("TSI.aNIC");
+ Log.startSession("TSI.aNIC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
Log.i(this, "Adding new incoming call with phoneAccountHandle %s",
phoneAccountHandle);
@@ -1292,7 +1516,7 @@ public class TelecomServiceImpl {
phoneAccountHandle.getComponentName() != null) {
if (isCallerSimCallManager(phoneAccountHandle)
&& TelephonyUtil.isPstnComponentName(
- phoneAccountHandle.getComponentName())) {
+ phoneAccountHandle.getComponentName())) {
Log.v(this, "Allowing call manager to add incoming call with PSTN" +
" handle");
} else {
@@ -1302,7 +1526,7 @@ public class TelecomServiceImpl {
// Make sure it doesn't cross the UserHandle boundary
enforceUserHandleMatchesCaller(phoneAccountHandle);
enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
- Binder.getCallingUserHandle());
+ phoneAccountHandle.getUserHandle());
if (isSelfManagedConnectionService(phoneAccountHandle)) {
// Self-managed phone account, ensure it has MANAGE_OWN_CALLS.
mContext.enforceCallingOrSelfPermission(
@@ -1344,9 +1568,10 @@ public class TelecomServiceImpl {
* @see android.telecom.TelecomManager#addNewIncomingConference
*/
@Override
- public void addNewIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+ public void addNewIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras,
+ String callingPackage) {
try {
- Log.startSession("TSI.aNIC");
+ Log.startSession("TSI.aNIC", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
Log.i(this, "Adding new incoming conference with phoneAccountHandle %s",
phoneAccountHandle);
@@ -1354,7 +1579,7 @@ public class TelecomServiceImpl {
phoneAccountHandle.getComponentName() != null) {
if (isCallerSimCallManager(phoneAccountHandle)
&& TelephonyUtil.isPstnComponentName(
- phoneAccountHandle.getComponentName())) {
+ phoneAccountHandle.getComponentName())) {
Log.v(this, "Allowing call manager to add incoming conference" +
" with PSTN handle");
} else {
@@ -1366,8 +1591,9 @@ public class TelecomServiceImpl {
enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
Binder.getCallingUserHandle());
if (isSelfManagedConnectionService(phoneAccountHandle)) {
- throw new SecurityException("Self-Managed ConnectionServices cannot add "
- + "adhoc conference calls");
+ throw new SecurityException(
+ "Self-Managed ConnectionServices cannot add "
+ + "adhoc conference calls");
}
}
long token = Binder.clearCallingIdentity();
@@ -1392,9 +1618,10 @@ public class TelecomServiceImpl {
* @see android.telecom.TelecomManager#acceptHandover
*/
@Override
- public void acceptHandover(Uri srcAddr, int videoState, PhoneAccountHandle destAcct) {
+ public void acceptHandover(Uri srcAddr, int videoState, PhoneAccountHandle destAcct,
+ String callingPackage) {
try {
- Log.startSession("TSI.aHO");
+ Log.startSession("TSI.aHO", Log.getPackageAbbreviation(callingPackage));
synchronized (mLock) {
Log.i(this, "acceptHandover; srcAddr=%s, videoState=%s, dest=%s",
Log.pii(srcAddr), VideoProfile.videoStateToString(videoState),
@@ -1475,7 +1702,8 @@ public class TelecomServiceImpl {
intent.putExtra(CallIntentProcessor.KEY_IS_UNKNOWN_CALL, true);
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
phoneAccountHandle);
- mCallIntentProcessorAdapter.processUnknownCallIntent(mCallsManager, intent);
+ mCallIntentProcessorAdapter.processUnknownCallIntent(mCallsManager,
+ intent);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1502,8 +1730,14 @@ public class TelecomServiceImpl {
throw new SecurityException("Package " + callingPackage + " is not allowed"
+ " to start conference call");
}
- mCallsManager.startConference(participants, extras, callingPackage,
- Binder.getCallingUserHandle());
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ mCallsManager.startConference(participants, extras, callingPackage,
+ Binder.getCallingUserHandle());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
} finally {
Log.endSession();
}
@@ -1520,7 +1754,6 @@ public class TelecomServiceImpl {
enforceCallingPackage(callingPackage, "placeCall");
PhoneAccountHandle phoneAccountHandle = null;
- boolean clearPhoneAccountHandleExtra = false;
if (extras != null) {
phoneAccountHandle = extras.getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
@@ -1529,31 +1762,42 @@ public class TelecomServiceImpl {
extras.remove(TelecomManager.EXTRA_IS_HANDOVER);
}
}
- boolean isSelfManaged = phoneAccountHandle != null &&
+ ComponentName phoneAccountComponentName = phoneAccountHandle != null
+ ? phoneAccountHandle.getComponentName() : null;
+ String phoneAccountPackageName = phoneAccountComponentName != null
+ ? phoneAccountComponentName.getPackageName() : null;
+ boolean isCallerOwnerOfPhoneAccount =
+ callingPackage.equals(phoneAccountPackageName);
+ boolean isSelfManagedPhoneAccount =
isSelfManagedConnectionService(phoneAccountHandle);
- if (isSelfManaged) {
- try {
- mContext.enforceCallingOrSelfPermission(
- Manifest.permission.MANAGE_OWN_CALLS,
- "Self-managed ConnectionServices require "
- + "MANAGE_OWN_CALLS permission.");
- } catch (SecurityException e) {
- // Fallback to use mobile network to avoid disclosing phone account handle
- // package information
- clearPhoneAccountHandleExtra = true;
- }
-
- if (!clearPhoneAccountHandleExtra && !callingPackage.equals(
- phoneAccountHandle.getComponentName().getPackageName())
- && !canCallPhone(callingPackage, callingFeatureId,
- "CALL_PHONE permission required to place calls.")) {
- // The caller is not allowed to place calls, so fallback to use mobile
- // network.
- clearPhoneAccountHandleExtra = true;
- }
- } else if (!canCallPhone(callingPackage, callingFeatureId, "placeCall")) {
- throw new SecurityException("Package " + callingPackage
- + " is not allowed to place phone calls");
+ // Ensure the app's calling package matches the PhoneAccount package name before
+ // checking self-managed status so that we do not leak installed package
+ // information.
+ boolean isSelfManagedRequest = isCallerOwnerOfPhoneAccount &&
+ isSelfManagedPhoneAccount;
+ if (isSelfManagedRequest) {
+ // The package name of the caller matches the package name of the
+ // PhoneAccountHandle, so ensure the app has MANAGE_OWN_CALLS permission if
+ // self-managed.
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_OWN_CALLS,
+ "Self-managed ConnectionServices require MANAGE_OWN_CALLS permission.");
+ } else if (!canCallPhone(callingPackage, callingFeatureId,
+ "CALL_PHONE permission required to place calls.")) {
+ // not self-managed, so CALL_PHONE is required.
+ mAnomalyReporter.reportAnomaly(PLACE_CALL_SECURITY_EXCEPTION_ERROR_UUID,
+ PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG);
+ throw new SecurityException(
+ "CALL_PHONE permission required to place calls.");
+ }
+
+ // An application can not place a call with a self-managed PhoneAccount that
+ // they do not own. If this is the case (and the app has CALL_PHONE permission),
+ // remove the PhoneAccount from the request and place the call as if it was a
+ // managed call request with no PhoneAccount specified.
+ if (!isCallerOwnerOfPhoneAccount && isSelfManagedPhoneAccount) {
+ // extras can not be null if isSelfManagedPhoneAccount is true
+ extras.remove(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
}
// Note: we can still get here for the default/system dialer, even if the Phone
@@ -1567,14 +1811,14 @@ public class TelecomServiceImpl {
Binder.getCallingUid(), callingPackage, callingFeatureId, null)
== AppOpsManager.MODE_ALLOWED;
- final boolean hasCallPermission = mContext.checkCallingPermission(CALL_PHONE) ==
- PackageManager.PERMISSION_GRANTED;
+ final boolean hasCallPermission = mContext.checkCallingOrSelfPermission(CALL_PHONE)
+ == PackageManager.PERMISSION_GRANTED;
// The Emergency Dialer has call privileged permission and uses this to place
// emergency calls. We ensure permission checks in
// NewOutgoingCallIntentBroadcaster#process pass by sending this to
// Telecom as an ACTION_CALL_PRIVILEGED intent (which makes sense since the
// com.android.phone process has that permission).
- final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
+ final boolean hasCallPrivilegedPermission = mContext.checkCallingOrSelfPermission(
CALL_PRIVILEGED) == PackageManager.PERMISSION_GRANTED;
synchronized (mLock) {
@@ -1584,16 +1828,13 @@ public class TelecomServiceImpl {
final Intent intent = new Intent(hasCallPrivilegedPermission ?
Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, handle);
if (extras != null) {
- if (clearPhoneAccountHandleExtra) {
- extras.remove(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
- }
extras.setDefusable(true);
intent.putExtras(extras);
}
mUserCallIntentProcessorFactory.create(mContext, userHandle)
- .processIntent(
- intent, callingPackage, isSelfManaged ||
- (hasCallAppOp && hasCallPermission),
+ .processIntent(intent, callingPackage, isSelfManagedRequest,
+ (hasCallAppOp && hasCallPermission)
+ || hasCallPrivilegedPermission,
true /* isLocalInvocation */);
} finally {
Binder.restoreCallingIdentity(token);
@@ -1633,10 +1874,11 @@ public class TelecomServiceImpl {
enforcePermission(MODIFY_PHONE_STATE);
enforcePermission(WRITE_SECURE_SETTINGS);
synchronized (mLock) {
+ int callerUserId = UserHandle.getCallingUserId();
long token = Binder.clearCallingIdentity();
try {
return mDefaultDialerCache.setDefaultDialer(packageName,
- ActivityManager.getCurrentUser());
+ callerUserId);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1680,11 +1922,12 @@ public class TelecomServiceImpl {
}
/**
- * Dumps the current state of the TelecomService. Used when generating problem reports.
+ * Dumps the current state of the TelecomService. Used when generating problem
+ * reports.
*
- * @param fd The file descriptor.
+ * @param fd The file descriptor.
* @param writer The print writer to dump the state to.
- * @param args Optional dump arguments.
+ * @param args Optional dump arguments.
*/
@Override
protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
@@ -1698,19 +1941,21 @@ public class TelecomServiceImpl {
}
- if (args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(args[0])) {
+ if (args != null && args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(
+ args[0])) {
Binder.withCleanCallingIdentity(() ->
Analytics.dumpToEncodedProto(mContext, writer, args));
return;
}
- boolean isTimeLineView = (args.length > 0 && TIME_LINE_ARG.equalsIgnoreCase(args[0]));
+ boolean isTimeLineView =
+ (args != null && args.length > 0 && TIME_LINE_ARG.equalsIgnoreCase(args[0]));
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
if (mCallsManager != null) {
pw.println("CallsManager: ");
pw.increaseIndent();
- mCallsManager.dump(pw);
+ mCallsManager.dump(pw, args);
pw.decreaseIndent();
pw.println("PhoneAccountRegistrar: ");
@@ -1734,8 +1979,13 @@ public class TelecomServiceImpl {
* @see android.telecom.TelecomManager#createManageBlockedNumbersIntent
*/
@Override
- public Intent createManageBlockedNumbersIntent() {
- return BlockedNumbersActivity.getIntentForStartingActivity();
+ public Intent createManageBlockedNumbersIntent(String callingPackage) {
+ try {
+ Log.startSession("TSI.cMBNI", Log.getPackageAbbreviation(callingPackage));
+ return BlockedNumbersActivity.getIntentForStartingActivity();
+ } finally {
+ Log.endSession();
+ }
}
@@ -1762,7 +2012,7 @@ public class TelecomServiceImpl {
@Override
public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle,
String callingPackage) {
- Log.startSession("TSI.iICP");
+ Log.startSession("TSI.iICP", Log.getPackageAbbreviation(callingPackage));
try {
enforceCallingPackage(callingPackage, "isIncomingCallPermitted");
enforcePhoneAccountHandleMatchesCaller(phoneAccountHandle, callingPackage);
@@ -1787,7 +2037,7 @@ public class TelecomServiceImpl {
@Override
public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle,
String callingPackage) {
- Log.startSession("TSI.iOCP");
+ Log.startSession("TSI.iOCP", Log.getPackageAbbreviation(callingPackage));
try {
enforceCallingPackage(callingPackage, "isOutgoingCallPermitted");
enforcePhoneAccountHandleMatchesCaller(phoneAccountHandle, callingPackage);
@@ -1900,9 +2150,12 @@ public class TelecomServiceImpl {
/**
* 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.
+ * {@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
* {@link android.telecom.CallDiagnosticService}s.
*/
@@ -1913,14 +2166,18 @@ public class TelecomServiceImpl {
synchronized (mLock) {
enforceShellOnly(Binder.getCallingUid(), "cleanupStuckCalls");
Binder.withCleanCallingIdentity(() -> {
+ Set<UserHandle> userHandles = new HashSet<>();
for (Call call : mCallsManager.getCalls()) {
call.cleanup();
if (call.getState() == CallState.DISCONNECTED
|| call.getState() == CallState.DISCONNECTING) {
mCallsManager.markCallAsRemoved(call);
}
+ userHandles.add(call.getAssociatedUser());
+ }
+ for (UserHandle userHandle : userHandles) {
+ mCallsManager.getInCallController().unbindFromServices(userHandle);
}
- mCallsManager.getInCallController().unbindFromServices();
});
}
} finally {
@@ -1930,7 +2187,8 @@ public class TelecomServiceImpl {
/**
* 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} or a
+ * {@link PhoneAccount} is a phone account belongs to an invalid {@link UserHandle}
+ * or a
* deleted package.
*
* @return the number of orphan {@code PhoneAccount} deleted.
@@ -2124,8 +2382,8 @@ public class TelecomServiceImpl {
* Determines whether there are any ongoing {@link PhoneAccount#CAPABILITY_SELF_MANAGED}
* calls for a given {@code packageName} and {@code userHandle}.
*
- * @param packageName the package name of the app to check calls for.
- * @param userHandle the user handle on which to check for calls.
+ * @param packageName the package name of the app to check calls for.
+ * @param userHandle the user handle on which to check for calls.
* @param callingPackage The caller's package name.
* @return {@code true} if there are ongoing calls, {@code false} otherwise.
*/
@@ -2154,6 +2412,20 @@ public class TelecomServiceImpl {
}
};
+ private boolean enforceCallStreamingPermission(String packageName, PhoneAccountHandle handle,
+ int uid) {
+ // TODO: implement this permission check (make sure the calling package is the d2di package)
+ PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(handle,
+ UserHandle.getUserHandleForUid(uid));
+ if (account == null
+ || !account.hasCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_CALL_STREAMING)) {
+ throw new SecurityException(
+ "The phone account handle in requesting can't support call streaming: "
+ + handle);
+ }
+ return true;
+ }
+
/**
* @return whether to return early without doing the action/throwing
* @throws SecurityException same as {@link Context#enforceCallingOrSelfPermission}
@@ -2204,6 +2476,8 @@ public class TelecomServiceImpl {
private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
private final SettingsSecureAdapter mSettingsSecureAdapter;
private final TelecomSystem.SyncRoot mLock;
+ private TransactionManager mTransactionManager;
+ private final TransactionalServiceRepository mTransactionalServiceRepository;
public TelecomServiceImpl(
Context context,
@@ -2240,6 +2514,14 @@ public class TelecomServiceImpl {
defaultDialer);
mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
});
+
+ mTransactionManager = TransactionManager.getInstance();
+ mTransactionalServiceRepository = new TransactionalServiceRepository();
+ }
+
+ @VisibleForTesting
+ public void setTransactionManager(TransactionManager transactionManager){
+ mTransactionManager = transactionManager;
}
public ITelecomService.Stub getBinder() {
@@ -2352,6 +2634,29 @@ public class TelecomServiceImpl {
}
}
+ // Enforce that the PhoneAccountHandle is tied to a self-managed package and not managed (aka
+ // sim calling, etc.)
+ private void enforcePhoneAccountIsNotManaged(PhoneAccountHandle phoneAccountHandle) {
+ PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle,
+ phoneAccountHandle.getUserHandle());
+ if (phoneAccount == null) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " phoneAccount is null");
+ }
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " CAPABILITY_SIM_SUBSCRIPTION is not allowed");
+ }
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " CAPABILITY_CALL_PROVIDER is not allowed");
+ }
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " CAPABILITY_CONNECTION_MANAGER is not allowed");
+ }
+ }
+
private void enforcePhoneAccountModificationForPackage(String packageName) {
// TODO: Use a new telecomm permission for this instead of reusing modify.
@@ -2383,21 +2688,89 @@ public class TelecomServiceImpl {
}
private void enforceCallingPackage(String packageName, String message) {
+ int callingUid = Binder.getCallingUid();
+
+ if (callingUid != Process.ROOT_UID &&
+ !callingUidMatchesPackageManagerRecords(packageName)) {
+ throw new SecurityException(message + ": Package " + packageName
+ + " does not belong to " + callingUid);
+ }
+ }
+
+ /**
+ * helper method that compares the binder_uid to what the packageManager_uid reports for the
+ * passed in packageName.
+ *
+ * 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 = mContext.createContextAsUser(
- UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
+ PackageManager pm;
+ try{
+ pm = mContext.createContextAsUser(
+ UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
+ }
+ catch (Exception e){
+ Log.i(this, "callingUidMatchesPackageManagerRecords:"
+ + " createContextAsUser hit exception=[%s]", e.toString());
+ return false;
+ }
if (pm != null) {
try {
packageUid = pm.getPackageUid(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
- // packageUid is -1
+ // packageUid is -1.
}
}
- if (packageUid != callingUid && callingUid != Process.ROOT_UID) {
- throw new SecurityException(message + ": Package " + packageName
- + " does not belong to " + callingUid);
+
+ if (packageUid != callingUid) {
+ Log.i(this, "callingUidMatchesPackageManagerRecords: uid mismatch found for"
+ + "packageName=[%s]. packageManager reports packageUid=[%d] but "
+ + "binder reports callingUid=[%d]", packageName, packageUid, callingUid);
}
+
+ return packageUid == callingUid;
+ }
+
+ /**
+ * Note: This method should be called BEFORE clearing the binder identity.
+ *
+ * @param permissionsToValidate set of permissions that should be checked
+ * @param alreadyComputedPermissions a list of permissions that were already checked
+ * @return all the permissions that
+ */
+ private Set<String> computePermissionsForBoundPackage(
+ Set<String> permissionsToValidate,
+ Set<String> alreadyComputedPermissions) {
+ Set<String> permissions = Objects.requireNonNullElseGet(alreadyComputedPermissions,
+ HashSet::new);
+ for (String permission : permissionsToValidate) {
+ if (mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
+ permissions.add(permission);
+ }
+ }
+ return permissions;
+ }
+
+ /**
+ * This method should be used to clear {@link PhoneAccount} properties based on a
+ * callingPackages permissions.
+ *
+ * @param account to clear properties from
+ * @param permissions the list of permissions the callingPackge has
+ * @return the account that callingPackage will receive
+ */
+ private PhoneAccount maybeCleansePhoneAccount(PhoneAccount account,
+ Set<String> permissions) {
+ if (account == null) {
+ return null;
+ }
+ PhoneAccount.Builder accountBuilder = new PhoneAccount.Builder(account);
+ if (!permissions.contains(MODIFY_PHONE_STATE)) {
+ accountBuilder.setGroupId("***");
+ }
+ return accountBuilder.build();
}
private void enforceTelecomFeature() {
@@ -2462,7 +2835,9 @@ public class TelecomServiceImpl {
private void enforceUserHandleMatchesCaller(PhoneAccountHandle accountHandle) {
if (!Binder.getCallingUserHandle().equals(accountHandle.getUserHandle())) {
- throw new SecurityException("Calling UserHandle does not match PhoneAccountHandle's");
+ // Enforce INTERACT_ACROSS_USERS if the calling user handle does not match
+ // phone account's user handle
+ enforceInAppCrossUserPermission();
}
}
@@ -2481,6 +2856,18 @@ public class TelecomServiceImpl {
}
}
+ private void enforceInAppCrossUserPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS, "Must be system or have"
+ + " INTERACT_ACROSS_USERS permission");
+ }
+
+ private boolean hasInAppCrossUserPermission() {
+ return mContext.checkCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS)
+ == 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) {
@@ -2619,6 +3006,11 @@ public class TelecomServiceImpl {
return true;
}
+ if (mContext.checkCallingOrSelfPermission(CALL_PRIVILEGED)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+
// Accessing phone state is gated by a special permission.
mContext.enforceCallingOrSelfPermission(CALL_PHONE, message);
@@ -2752,4 +3144,22 @@ public class TelecomServiceImpl {
mContext.sendBroadcast(intent);
}
}
+
+ private void validateAccountIconUserBoundary(Icon icon) {
+ // Refer to Icon#getUriString for context. The URI string is invalid for icons of
+ // incompatible types.
+ if (icon != null && (icon.getType() == Icon.TYPE_URI
+ || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
+ String encodedUser = icon.getUri().getEncodedUserInfo();
+ // If there is no encoded user, the URI is calling into the calling user space
+ if (encodedUser != null) {
+ int userId = Integer.parseInt(encodedUser);
+ if (userId != UserHandle.getUserId(Binder.getCallingUid())) {
+ // If we are transcending the profile boundary, throw an error.
+ throw new IllegalArgumentException("Attempting to register a phone account with"
+ + " an image icon belonging to another user.");
+ }
+ }
+ }
+ }
}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 237f03970..67bb81f55 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -16,43 +16,50 @@
package com.android.server.telecom;
-import com.android.internal.annotations.VisibleForTesting;
-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.components.UserCallIntentProcessor;
-import com.android.server.telecom.components.UserCallIntentProcessorFactory;
-import com.android.server.telecom.ui.AudioProcessingNotification;
-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.CallAudioManager.AudioServiceFactory;
-import com.android.server.telecom.DefaultDialerCache.DefaultDialerManagerAdapter;
-import com.android.server.telecom.ui.ToastFactory;
-
+import android.Manifest;
import android.app.ActivityManager;
import android.bluetooth.BluetoothManager;
-import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+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;
+import android.telephony.TelephonyManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.CallAudioManager.AudioServiceFactory;
+import com.android.server.telecom.DefaultDialerCache.DefaultDialerManagerAdapter;
+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.components.UserCallIntentProcessor;
+import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+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 java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
/**
* Top-level Application class for Telecom.
@@ -109,6 +116,11 @@ public class TelecomSystem {
.addDataAuthority(DialerCodeReceiver.TELECOM_SECRET_CODE_MARK, null);
DIALER_SECRET_CODE_FILTER
.addDataAuthority(DialerCodeReceiver.TELECOM_SECRET_CODE_MENU, null);
+
+ USER_SWITCHED_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ USER_STARTING_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ BOOT_COMPLETE_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ DIALER_SECRET_CODE_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
}
private static TelecomSystem INSTANCE = null;
@@ -208,9 +220,14 @@ public class TelecomSystem {
ClockProxy clockProxy,
RoleManagerAdapter roleManagerAdapter,
ContactsAsyncHelper.Factory contactsAsyncHelperFactory,
- DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
+ DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+ Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
+ Executor asyncTaskExecutor,
+ BlockedNumbersAdapter blockedNumbersAdapter) {
mContext = context.getApplicationContext();
LogUtils.initLogging(mContext);
+ android.telecom.Log.setLock(mLock);
+ AnomalyReporter.initialize(mContext);
DefaultDialerManagerAdapter defaultDialerAdapter =
new DefaultDialerCache.DefaultDialerManagerAdapterImpl();
@@ -269,6 +286,15 @@ public class TelecomSystem {
}
};
+ CallEndpointControllerFactory callEndpointControllerFactory =
+ new CallEndpointControllerFactory() {
+ @Override
+ public CallEndpointController create(Context context, SyncRoot lock,
+ CallsManager callsManager) {
+ return new CallEndpointController(context, callsManager);
+ }
+ };
+
CallDiagnosticServiceController callDiagnosticServiceController =
new CallDiagnosticServiceController(
new CallDiagnosticServiceController.ContextProxy() {
@@ -319,6 +345,23 @@ public class TelecomSystem {
}
};
+ EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger =
+ new EmergencyCallDiagnosticLogger(mContext.getSystemService(
+ TelephonyManager.class), mContext.getSystemService(
+ BugreportManager.class), timeoutsAdapter, mContext.getSystemService(
+ DropBoxManager.class), asyncTaskExecutor, clockProxy);
+
+ CallAnomalyWatchdog callAnomalyWatchdog = new CallAnomalyWatchdog(
+ Executors.newSingleThreadScheduledExecutor(),
+ mLock, timeoutsAdapter, clockProxy, emergencyCallDiagnosticLogger);
+
+ TransactionManager transactionManager = TransactionManager.getInstance();
+
+ CallStreamingNotification callStreamingNotification =
+ new CallStreamingNotification(mContext,
+ packageName -> AppLabelProxy.Util.getAppLabel(
+ mContext.getPackageManager(), packageName), asyncTaskExecutor);
+
mCallsManager = new CallsManager(
mContext,
mLock,
@@ -348,7 +391,15 @@ public class TelecomSystem {
inCallControllerFactory,
callDiagnosticServiceController,
roleManagerAdapter,
- toastFactory);
+ toastFactory,
+ callEndpointControllerFactory,
+ callAnomalyWatchdog,
+ accessibilityManagerAdapter,
+ asyncTaskExecutor,
+ blockedNumbersAdapter,
+ transactionManager,
+ emergencyCallDiagnosticLogger,
+ callStreamingNotification);
mIncomingCallNotifier = incomingCallNotifier;
incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 36caa2598..c5fdd4c26 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -20,8 +20,8 @@ import android.content.ContentResolver;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.telecom.CallDiagnosticService;
-import android.telecom.CallRedirectionService;
import android.telecom.CallDiagnostics;
+import android.telecom.CallRedirectionService;
import android.telephony.ims.ImsReasonInfo;
import java.util.concurrent.TimeUnit;
@@ -78,13 +78,135 @@ public final class Timeouts {
}
public long getCallStartAppOpDebounceIntervalMillis() {
- return Timeouts.getCallStartAppOpDebounceIntervalMillis();
+ return Timeouts.getCallStartAppOpDebounceIntervalMillis();
+ }
+
+ public long getVoipCallTransitoryStateTimeoutMillis() {
+ return Timeouts.getVoipCallTransitoryStateTimeoutMillis();
+ }
+
+ public long getVoipEmergencyCallTransitoryStateTimeoutMillis() {
+ return Timeouts.getVoipEmergencyCallTransitoryStateTimeoutMillis();
+ }
+
+ public long getNonVoipCallTransitoryStateTimeoutMillis() {
+ return Timeouts.getNonVoipCallTransitoryStateTimeoutMillis();
+ }
+
+ public long getNonVoipEmergencyCallTransitoryStateTimeoutMillis() {
+ return Timeouts.getNonVoipEmergencyCallTransitoryStateTimeoutMillis();
+ }
+
+ public long getVoipCallIntermediateStateTimeoutMillis() {
+ return Timeouts.getVoipCallIntermediateStateTimeoutMillis();
+ }
+
+ public long getVoipEmergencyCallIntermediateStateTimeoutMillis() {
+ return Timeouts.getVoipEmergencyCallIntermediateStateTimeoutMillis();
+ }
+
+ public long getNonVoipCallIntermediateStateTimeoutMillis() {
+ return Timeouts.getNonVoipCallIntermediateStateTimeoutMillis();
+ }
+
+ public long getNonVoipEmergencyCallIntermediateStateTimeoutMillis() {
+ return Timeouts.getNonVoipEmergencyCallIntermediateStateTimeoutMillis();
+ }
+
+ public long getEmergencyCallTimeBeforeUserDisconnectThresholdMillis(){
+ return Timeouts.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis();
+ }
+
+ public long getEmergencyCallActiveTimeThresholdMillis(){
+ return Timeouts.getEmergencyCallActiveTimeThresholdMillis();
+ }
+
+ public int getDaysBackToSearchEmergencyDiagnosticEntries(){
+ return Timeouts.getDaysBackToSearchEmergencyDiagnosticEntries();
+
}
}
/** A prefix to use for all keys so to not clobber the global namespace. */
private static final String PREFIX = "telecom.";
+ /**
+ * threshold used to filter out ecalls that the user may have dialed by mistake
+ * It is used only when the disconnect cause is LOCAL by EmergencyDiagnosticLogger
+ */
+ private static final String EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS =
+ "emergency_call_time_before_user_disconnect_threshold_millis";
+
+ /**
+ * Returns the threshold used to detect ecalls that transition to active but only for a very
+ * short duration. These short duration active calls can result in Diagnostic data collection.
+ */
+ private static final String EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS =
+ "emergency_call_active_time_threshold_millis";
+
+ /**
+ * Time in Days that is used to filter out old dropbox entries for emergency call diagnostic
+ * data. Entries older than this are ignored
+ */
+ private static final String DAYS_BACK_TO_SEARCH_EMERGENCY_DROP_BOX_ENTRIES =
+ "days_back_to_search_emergency_drop_box_entries";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the transitory state timeout of
+ * VoIP Call, in millis.
+ */
+ private static final String TRANSITORY_STATE_VOIP_NORMAL_TIMEOUT_MILLIS =
+ "transitory_state_voip_normal_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the transitory state timeout of
+ * VoIP emergency Call, in millis.
+ */
+ private static final String TRANSITORY_STATE_VOIP_EMERGENCY_TIMEOUT_MILLIS =
+ "transitory_state_voip_emergency_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the transitory state timeout of
+ * non-VoIP call, in millis.
+ */
+ private static final String TRANSITORY_STATE_NON_VOIP_NORMAL_TIMEOUT_MILLIS =
+ "transitory_state_non_voip_normal_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the transitory state timeout of
+ * non-VoIP emergency call, in millis.
+ */
+ private static final String TRANSITORY_STATE_NON_VOIP_EMERGENCY_TIMEOUT_MILLIS =
+ "transitory_state_non_voip_emergency_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the intermediate state timeout of
+ * VoIP call, in millis.
+ */
+ private static final String INTERMEDIATE_STATE_VOIP_NORMAL_TIMEOUT_MILLIS =
+ "intermediate_state_voip_normal_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the intermediate state timeout of
+ * VoIP emergency call, in millis.
+ */
+ private static final String INTERMEDIATE_STATE_VOIP_EMERGENCY_TIMEOUT_MILLIS =
+ "intermediate_state_voip_emergency_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the intermediate state timeout of
+ * non-VoIP call, in millis.
+ */
+ private static final String INTERMEDIATE_STATE_NON_VOIP_NORMAL_TIMEOUT_MILLIS =
+ "intermediate_state_non_voip_normal_timeout_millis";
+
+ /**
+ * A prefix to use for {@link DeviceConfig} for the intermediate state timeout of
+ * non-VoIP emergency call, in millis.
+ */
+ private static final String INTERMEDIATE_STATE_NON_VOIP_EMERGENCY_TIMEOUT_MILLIS =
+ "intermediate_state_non_voip_emergency_timeout_millis";
+
private Timeouts() {
}
@@ -237,6 +359,116 @@ public final class Timeouts {
return get(contentResolver, "call_diagnostic_service_timeout", 2000L /* 2 sec */);
}
+ /**
+ * Returns the duration of time a VoIP call can be in a transitory state before Telecom will
+ * try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getVoipCallTransitoryStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ TRANSITORY_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 5000L);
+ }
+
+
+ /**
+ * Returns the threshold used to filter out ecalls that the user may have dialed by mistake
+ * It is used only when the disconnect cause is LOCAL by EmergencyDiagnosticLogger
+ * @return the threshold in milliseconds
+ */
+ public static long getEmergencyCallTimeBeforeUserDisconnectThresholdMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS, 20000L);
+ }
+
+ /**
+ * Returns the threshold used to detect ecalls that transition to active but only for a very
+ * short duration. These short duration active calls can result in Diagnostic data collection.
+ * @return the threshold in milliseconds
+ */
+ public static long getEmergencyCallActiveTimeThresholdMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS, 15000L);
+ }
+
+ /**
+ * Time in Days that is used to filter out old dropbox entries for emergency call diagnostic
+ * data. Entries older than this are ignored
+ */
+ public static int getDaysBackToSearchEmergencyDiagnosticEntries() {
+ return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY,
+ DAYS_BACK_TO_SEARCH_EMERGENCY_DROP_BOX_ENTRIES, 30);
+ }
+
+ /**
+ * Returns the duration of time an emergency VoIP call can be in a transitory state before
+ * Telecom will try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getVoipEmergencyCallTransitoryStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ TRANSITORY_STATE_VOIP_EMERGENCY_TIMEOUT_MILLIS, 5000L);
+ }
+
+ /**
+ * Returns the duration of time a non-VoIP call can be in a transitory state before Telecom
+ * will try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getNonVoipCallTransitoryStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ TRANSITORY_STATE_NON_VOIP_NORMAL_TIMEOUT_MILLIS, 10000L);
+ }
+
+ /**
+ * Returns the duration of time an emergency non-VoIp call can be in a transitory state before
+ * Telecom will try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getNonVoipEmergencyCallTransitoryStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ TRANSITORY_STATE_NON_VOIP_EMERGENCY_TIMEOUT_MILLIS, 10000L);
+ }
+
+ /**
+ * Returns the duration of time a VoIP call can be in an intermediate state before Telecom will
+ * try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getVoipCallIntermediateStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ INTERMEDIATE_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 60000L);
+ }
+
+ /**
+ * Returns the duration of time an emergency VoIP call can be in an intermediate state before
+ * Telecom will try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getVoipEmergencyCallIntermediateStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ INTERMEDIATE_STATE_VOIP_EMERGENCY_TIMEOUT_MILLIS, 60000L);
+ }
+
+ /**
+ * Returns the duration of time a non-VoIP call can be in an intermediate state before Telecom
+ * will try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getNonVoipCallIntermediateStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ INTERMEDIATE_STATE_NON_VOIP_NORMAL_TIMEOUT_MILLIS, 120000L);
+ }
+
+ /**
+ * Returns the duration of time an emergency non-VoIP call can be in an intermediate state
+ * before Telecom will try to clean up the call.
+ * @return the state timeout in millis.
+ */
+ public static long getNonVoipEmergencyCallIntermediateStateTimeoutMillis() {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
+ INTERMEDIATE_STATE_NON_VOIP_EMERGENCY_TIMEOUT_MILLIS, 60000L);
+ }
+
public static long getCallStartAppOpDebounceIntervalMillis() {
return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY, "app_op_debounce_time", 250L);
}
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
new file mode 100644
index 000000000..15278e11b
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.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;
+
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.telecom.ICallEventCallback;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tracks all TransactionalServiceWrappers that have an ongoing call. Removes wrappers that have no
+ * more calls.
+ */
+public class TransactionalServiceRepository {
+ private static final String TAG = TransactionalServiceRepository.class.getSimpleName();
+ private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
+ new HashMap<>();
+
+ public TransactionalServiceRepository() {
+ }
+
+ public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
+ (PhoneAccountHandle phoneAccountHandle, ICallEventCallback callEventCallback,
+ CallsManager callsManager, Call call) {
+ TransactionalServiceWrapper service;
+ // Only create a new TransactionalServiceWrapper if this is the first call for a package.
+ // Otherwise, get the existing TSW and add the new call to the service.
+ if (!hasExistingServiceWrapper(phoneAccountHandle)) {
+ Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
+ service = new TransactionalServiceWrapper(callEventCallback,
+ callsManager, phoneAccountHandle, call, this);
+ } else {
+ Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
+ service = getTransactionalServiceWrapper(phoneAccountHandle);
+ if (service == null) {
+ throw new IllegalStateException("service is null");
+ } else {
+ service.trackCall(call);
+ }
+ }
+
+ mServiceLookupTable.put(phoneAccountHandle, service);
+
+ return service;
+ }
+
+ public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+ return mServiceLookupTable.get(pah);
+ }
+
+ public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ return mServiceLookupTable.containsKey(pah);
+ }
+
+ public boolean removeServiceWrapper(PhoneAccountHandle pah) {
+ Log.i(TAG, "removeServiceWrapper: for phoneAccountHandle=[%s]", pah);
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ mServiceLookupTable.remove(pah);
+ return true;
+ }
+}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
new file mode 100644
index 000000000..25aaad789
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -0,0 +1,659 @@
+/*
+ * 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 android.telecom.CallException.CODE_CALL_IS_NOT_BEING_TRACKED;
+import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.telecom.CallEndpoint;
+import android.telecom.CallException;
+import android.telecom.CallStreamingService;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Implements {@link android.telecom.CallEventCallback} and {@link android.telecom.CallControl}
+ * on a per-client basis which is tied to a {@link PhoneAccountHandle}
+ */
+public class TransactionalServiceWrapper implements
+ ConnectionServiceFocusManager.ConnectionServiceFocus {
+ private static final String TAG = TransactionalServiceWrapper.class.getSimpleName();
+
+ // CallControl : Client (ex. voip app) --> Telecom
+ public static final String SET_ACTIVE = "SetActive";
+ public static final String SET_INACTIVE = "SetInactive";
+ public static final String ANSWER = "Answer";
+ public static final String DISCONNECT = "Disconnect";
+ public static final String START_STREAMING = "StartStreaming";
+
+ // CallEventCallback : Telecom --> Client (ex. voip app)
+ public static final String ON_SET_ACTIVE = "onSetActive";
+ public static final String ON_SET_INACTIVE = "onSetInactive";
+ public static final String ON_ANSWER = "onAnswer";
+ public static final String ON_DISCONNECT = "onDisconnect";
+ public static final String ON_STREAMING_STARTED = "onStreamingStarted";
+
+ private final CallsManager mCallsManager;
+ private final ICallEventCallback mICallEventCallback;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ private final TransactionalServiceRepository mRepository;
+ private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
+ // init when constructor is called
+ private final ConcurrentHashMap<String, Call> mTrackedCalls = new ConcurrentHashMap<>();
+ private final TelecomSystem.SyncRoot mLock;
+ private final String mPackageName;
+ // needs to be non-final for testing
+ private TransactionManager mTransactionManager;
+ private CallStreamingController mStreamingController;
+
+
+ // Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
+ // any calls in the event the application crashes or is force stopped.
+ private final IBinder.DeathRecipient mAppDeathListener = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ Log.i(TAG, "binderDied: for package=[%s]; cleaning calls", mPackageName);
+ cleanupTransactionalServiceWrapper();
+ mICallEventCallback.asBinder().unlinkToDeath(this, 0);
+ }
+ };
+
+ public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
+ CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
+ TransactionalServiceRepository repo) {
+ // passed args
+ mICallEventCallback = callEventCallback;
+ mCallsManager = callsManager;
+ mPhoneAccountHandle = phoneAccountHandle;
+ mTrackedCalls.put(call.getId(), call); // service is now tracking its first call
+ mRepository = repo;
+ // init instance vars
+ mPackageName = phoneAccountHandle.getComponentName().getPackageName();
+ mTransactionManager = TransactionManager.getInstance();
+ mStreamingController = mCallsManager.getCallStreamingController();
+ mLock = mCallsManager.getLock();
+ setDeathRecipient(callEventCallback);
+ }
+
+ @VisibleForTesting
+ public void setTransactionManager(TransactionManager transactionManager) {
+ mTransactionManager = transactionManager;
+ }
+
+ public TransactionManager getTransactionManager() {
+ return mTransactionManager;
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ public void trackCall(Call call) {
+ synchronized (mLock) {
+ if (call != null) {
+ mTrackedCalls.put(call.getId(), call);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public boolean untrackCall(Call call) {
+ Call removedCall = null;
+ synchronized (mLock) {
+ if (call != null) {
+ removedCall = mTrackedCalls.remove(call.getId());
+ if (mTrackedCalls.size() == 0) {
+ mRepository.removeServiceWrapper(mPhoneAccountHandle);
+ }
+ }
+ }
+ Log.i(TAG, "removedCall call=" + removedCall);
+ return removedCall != null;
+ }
+
+ @VisibleForTesting
+ public int getNumberOfTrackedCalls() {
+ int callCount = 0;
+ synchronized (mLock) {
+ callCount = mTrackedCalls.size();
+ }
+ 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
+ }
+ }
+
+ /***
+ *********************************************************************************************
+ ** ICallControl: Client --> Server **
+ **********************************************************************************************
+ */
+ public final ICallControl mICallControl = new ICallControl.Stub() {
+ @Override
+ public void setActive(String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.sA");
+ createTransactions(callId, callback, SET_ACTIVE);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void answer(int videoState, String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.a");
+ createTransactions(callId, callback, ANSWER, videoState);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void setInactive(String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.sI");
+ createTransactions(callId, callback, SET_INACTIVE);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void disconnect(String callId, DisconnectCause disconnectCause,
+ android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.d");
+ createTransactions(callId, callback, DISCONNECT, disconnectCause);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void startCallStreaming(String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.sCS");
+ createTransactions(callId, callback, START_STREAMING);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ private void createTransactions(String callId, ResultReceiver callback, String action,
+ Object... objects) {
+ Log.d(TAG, "createTransactions: callId=" + callId);
+ Call call = mTrackedCalls.get(callId);
+ if (call != null) {
+ switch (action) {
+ case SET_ACTIVE:
+ handleCallControlNewCallFocusTransactions(call, SET_ACTIVE,
+ false /* isAnswer */, 0/*VideoState (ignored)*/, callback);
+ break;
+ case ANSWER:
+ handleCallControlNewCallFocusTransactions(call, ANSWER,
+ true /* isAnswer */, (int) objects[0] /*VideoState*/, callback);
+ break;
+ case DISCONNECT:
+ addTransactionsToManager(new EndCallTransaction(mCallsManager,
+ (DisconnectCause) objects[0], call), callback);
+ break;
+ case SET_INACTIVE:
+ addTransactionsToManager(
+ new HoldCallTransaction(mCallsManager, call), callback);
+ break;
+ case START_STREAMING:
+ addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
+ TransactionalServiceWrapper.this, call, mLock), callback);
+ break;
+ }
+ } else {
+ Bundle exceptionBundle = new Bundle();
+ exceptionBundle.putParcelable(TRANSACTION_EXCEPTION_KEY,
+ new CallException(TextUtils.formatSimple(
+ "Telecom cannot process [%s] because the call with id=[%s] is no longer "
+ + "being tracked. This is most likely a result of the call "
+ + "already being disconnected and removed. Try re-adding the call"
+ + " via TelecomManager#addCall", action, callId),
+ CODE_CALL_IS_NOT_BEING_TRACKED));
+ callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
+ }
+ }
+
+ // 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) {
+ try {
+ Log.startSession("TSW.rCEC");
+ addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
+ callback);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ /**
+ * Application would like to inform InCallServices of an event
+ */
+ @Override
+ public void sendEvent(String callId, String event, Bundle extras) {
+ try {
+ Log.startSession("TSW.sE");
+ Call call = mTrackedCalls.get(callId);
+ if (call != null) {
+ call.onConnectionEvent(event, extras);
+ } else {
+ Log.i(TAG,
+ "sendEvent: was called but there is no call with id=[%s] cannot be "
+ + "found. Most likely the call has been disconnected");
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+
+ public void addTransactionsToManager(VoipCallTransaction transaction,
+ ResultReceiver callback) {
+ Log.d(TAG, "addTransactionsToManager");
+
+ mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ Log.d(TAG, "addTransactionsToManager: onResult:");
+ callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.d(TAG, "addTransactionsToManager: onError");
+ Bundle extras = new Bundle();
+ extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
+ callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
+ exception.getCode(), extras);
+ }
+ });
+ }
+
+ public ICallControl getICallControl() {
+ return mICallControl;
+ }
+
+ /***
+ *********************************************************************************************
+ ** ICallEventCallback: Server --> Client **
+ **********************************************************************************************
+ */
+
+ public void onSetActive(Call call) {
+ 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*/);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ 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*/);
+ } 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) {
+ 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<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ mCallsManager.markCallAsOnHold(call);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception);
+ }
+ });
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ public void onDisconnect(Call call, DisconnectCause cause) {
+ 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);
+ }
+ }
+ );
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ public void onCallStreamingStarted(Call call) {
+ try {
+ Log.startSession("TSW.oCSS");
+ Log.d(TAG, String.format(Locale.US, "onCallStreamingStarted: callId=[%s]",
+ call.getId()));
+
+ mTransactionManager.addTransaction(
+ new CallEventCallbackAckTransaction(mICallEventCallback, ON_STREAMING_STARTED,
+ call.getId(), mLock), new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, "onCallStreamingStarted: onError: with e=[%e]",
+ exception);
+ stopCallStreaming(call);
+ }
+ }
+ );
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ public void onCallStreamingFailed(Call call,
+ @CallStreamingService.StreamingFailedReason int streamingFailedReason) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onCallStreamingFailed(call.getId(), streamingFailedReason);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onCallEndpointChanged(call.getId(), endpoint);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onAvailableCallEndpointsChanged(call.getId(),
+ endpoints.stream().toList());
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ public void onMuteStateChanged(Call call, boolean isMuted) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onMuteStateChanged(call.getId(), isMuted);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ public void removeCallFromWrappers(Call call) {
+ if (call != null) {
+ try {
+ // remove the call from frameworks wrapper (client side)
+ mICallEventCallback.removeCallFromTransactionalServiceWrapper(call.getId());
+ } catch (RemoteException e) {
+ }
+ // remove the call from this class/wrapper (server side)
+ untrackCall(call);
+ }
+ }
+
+ public void onEvent(Call call, String event, Bundle extras) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onEvent(call.getId(), event, extras);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /***
+ *********************************************************************************************
+ ** 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);
+ } catch (Exception e) {
+ Log.w(TAG, "setDeathRecipient: hit exception=[%s] trying to link binder to death",
+ e.toString());
+ }
+ }
+
+ /***
+ *********************************************************************************************
+ ** FocusManager **
+ **********************************************************************************************
+ */
+
+ @Override
+ public void connectionServiceFocusLost() {
+ if (mConnSvrFocusListener != null) {
+ mConnSvrFocusListener.onConnectionServiceReleased(this);
+ }
+ Log.i(TAG, String.format(Locale.US, "connectionServiceFocusLost for package=[%s]",
+ mPackageName));
+ }
+
+ @Override
+ public void connectionServiceFocusGained() {
+ Log.i(TAG, String.format(Locale.US, "connectionServiceFocusGained for package=[%s]",
+ mPackageName));
+ }
+
+ @Override
+ public void setConnectionServiceFocusListener(
+ ConnectionServiceFocusManager.ConnectionServiceFocusListener listener) {
+ mConnSvrFocusListener = listener;
+ }
+
+ @Override
+ public ComponentName getComponentName() {
+ return mPhoneAccountHandle.getComponentName();
+ }
+
+ /***
+ *********************************************************************************************
+ ** CallStreaming **
+ *********************************************************************************************
+ */
+
+ public void stopCallStreaming(Call call) {
+ Log.i(this, "stopCallStreaming; callid=%s", call.getId());
+ if (call != null && call.isStreaming()) {
+ VoipCallTransaction stopStreamingTransaction = mStreamingController
+ .getStopStreamingTransaction(call, mLock);
+ addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/TtyManager.java b/src/com/android/server/telecom/TtyManager.java
index 10e3348a4..53e917d7e 100644
--- a/src/com/android/server/telecom/TtyManager.java
+++ b/src/com/android/server/telecom/TtyManager.java
@@ -50,6 +50,7 @@ final class TtyManager implements WiredHeadsetManager.Listener {
IntentFilter intentFilter = new IntentFilter(
TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+ intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiver(mReceiver, intentFilter,
android.Manifest.permission.MODIFY_PHONE_STATE,
null, Context.RECEIVER_EXPORTED);
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 8fbb1fe80..09b8f76a3 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -48,6 +48,7 @@ public class BluetoothStateReceiver extends BroadcastReceiver {
INTENT_FILTER.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
INTENT_FILTER.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
INTENT_FILTER.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
+ INTENT_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
}
// If not in a call, BSR won't listen to the Bluetooth stack's HFP on/off messages, since
diff --git a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
new file mode 100644
index 000000000..b8658d836
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Adapter interface that wraps methods from
+ * {@link android.provider.BlockedNumberContract.SystemContract} and
+ * {@link com.android.server.telecom.settings.BlockedNumbersUtil} to make things testable.
+ */
+public interface BlockedNumbersAdapter {
+ boolean shouldShowEmergencyCallNotification (Context context);
+ void updateEmergencyCallNotification(Context context, boolean showNotification);
+}
diff --git a/src/com/android/server/telecom/callfiltering/CallFilteringResult.java b/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
index 84ce4d4e2..931d5bb64 100644
--- a/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
+++ b/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
@@ -29,6 +29,7 @@ public class CallFilteringResult {
private boolean mShouldReject;
private boolean mShouldAddToCallLog;
private boolean mShouldShowNotification;
+ private boolean mDndSuppressed = false;
private boolean mShouldSilence = false;
private boolean mShouldScreenViaAudio = false;
private boolean mContactExists = false;
@@ -58,6 +59,11 @@ public class CallFilteringResult {
return this;
}
+ public Builder setDndSuppressed(boolean shouldPerformCheck) {
+ mDndSuppressed = shouldPerformCheck;
+ return this;
+ }
+
public Builder setShouldSilence(boolean shouldSilence) {
mShouldSilence = shouldSilence;
return this;
@@ -101,6 +107,7 @@ public class CallFilteringResult {
.setShouldReject(result.shouldReject)
.setShouldAddToCallLog(result.shouldAddToCallLog)
.setShouldShowNotification(result.shouldShowNotification)
+ .setDndSuppressed(result.shouldSuppressCallDueToDndStatus)
.setShouldSilence(result.shouldSilence)
.setCallBlockReason(result.mCallBlockReason)
.setShouldScreenViaAudio(result.shouldScreenViaAudio)
@@ -113,8 +120,9 @@ public class CallFilteringResult {
public CallFilteringResult build() {
return new CallFilteringResult(mShouldAllowCall, mShouldReject, mShouldSilence,
- mShouldAddToCallLog, mShouldShowNotification, mCallBlockReason,
- mCallScreeningAppName, mCallScreeningComponentName, mCallScreeningResponse,
+ mShouldAddToCallLog, mShouldShowNotification,
+ mDndSuppressed, mCallBlockReason, mCallScreeningAppName,
+ mCallScreeningComponentName, mCallScreeningResponse,
mIsResponseFromSystemDialer, mShouldScreenViaAudio, mContactExists);
}
}
@@ -125,6 +133,7 @@ public class CallFilteringResult {
public boolean shouldAddToCallLog;
public boolean shouldScreenViaAudio = false;
public boolean shouldShowNotification;
+ public boolean shouldSuppressCallDueToDndStatus = false;
public int mCallBlockReason;
public CharSequence mCallScreeningAppName;
public String mCallScreeningComponentName;
@@ -133,8 +142,9 @@ public class CallFilteringResult {
public boolean contactExists;
private CallFilteringResult(boolean shouldAllowCall, boolean shouldReject, boolean
- shouldSilence, boolean shouldAddToCallLog, boolean shouldShowNotification, int
- callBlockReason, CharSequence callScreeningAppName, String callScreeningComponentName,
+ shouldSilence, boolean shouldAddToCallLog, boolean shouldShowNotification, boolean
+ shouldSuppress, int callBlockReason, CharSequence callScreeningAppName,
+ String callScreeningComponentName,
CallScreeningService.ParcelableCallResponse callScreeningResponse,
boolean isResponseFromSystemDialer,
boolean shouldScreenViaAudio, boolean contactExists) {
@@ -143,6 +153,7 @@ public class CallFilteringResult {
this.shouldSilence = shouldSilence;
this.shouldAddToCallLog = shouldAddToCallLog;
this.shouldShowNotification = shouldShowNotification;
+ this.shouldSuppressCallDueToDndStatus = shouldSuppress;
this.shouldScreenViaAudio = shouldScreenViaAudio;
this.mCallBlockReason = callBlockReason;
this.mCallScreeningAppName = callScreeningAppName;
@@ -202,6 +213,8 @@ public class CallFilteringResult {
.setShouldAddToCallLog(shouldAddToCallLog && other.shouldAddToCallLog)
.setShouldShowNotification(shouldShowNotification && other.shouldShowNotification)
.setShouldScreenViaAudio(shouldScreenViaAudio || other.shouldScreenViaAudio)
+ .setDndSuppressed(shouldSuppressCallDueToDndStatus
+ || other.shouldSuppressCallDueToDndStatus)
.setContactExists(contactExists || other.contactExists);
combineScreeningResponses(b, this, other);
return b.build();
@@ -228,6 +241,8 @@ public class CallFilteringResult {
.setShouldSilence(shouldSilence || other.shouldSilence)
.setShouldAddToCallLog(shouldAddToCallLog && other.shouldAddToCallLog)
.setShouldShowNotification(shouldShowNotification && other.shouldShowNotification)
+ .setDndSuppressed(shouldSuppressCallDueToDndStatus
+ || other.shouldSuppressCallDueToDndStatus)
.setShouldScreenViaAudio(shouldScreenViaAudio || other.shouldScreenViaAudio)
.setCallBlockReason(callBlockReason)
.setCallScreeningAppName(callScreeningAppName)
@@ -272,6 +287,7 @@ public class CallFilteringResult {
if (shouldSilence != that.shouldSilence) return false;
if (shouldAddToCallLog != that.shouldAddToCallLog) return false;
if (shouldShowNotification != that.shouldShowNotification) return false;
+ if (shouldSuppressCallDueToDndStatus != that.shouldSuppressCallDueToDndStatus) return false;
if (mCallBlockReason != that.mCallBlockReason) return false;
if (contactExists != that.contactExists) return false;
@@ -318,6 +334,10 @@ public class CallFilteringResult {
sb.append(", notified");
}
+ if (shouldSuppressCallDueToDndStatus) {
+ sb.append(", DND suppressed");
+ }
+
if (contactExists) {
sb.append(", contact exists");
}
diff --git a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index 5f4c40b82..f07c0aa42 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -24,6 +24,7 @@ import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.CallScreeningService;
import android.telecom.Log;
@@ -318,7 +319,7 @@ public class CallScreeningServiceFilter extends CallFilter {
CallScreeningServiceConnection connection = new CallScreeningServiceConnection(
resultFuture);
if (!CallScreeningServiceHelper.bindCallScreeningService(mContext,
- mCallsManager.getCurrentUserHandle(), mPackageName, connection)) {
+ mCall.getAssociatedUser(), mPackageName, connection)) {
Log.i(this, "Call screening service binding failed.");
resultFuture.complete(mPriorStageResult);
} else {
diff --git a/src/com/android/server/telecom/callfiltering/DndCallFilter.java b/src/com/android/server/telecom/callfiltering/DndCallFilter.java
new file mode 100644
index 000000000..f6ed64627
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/DndCallFilter.java
@@ -0,0 +1,72 @@
+/*
+ * 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.callfiltering;
+
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.Ringer;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * DndCallFilter is a incoming call filter that adds the
+ * {@link android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB } early in the call processing.
+ * Adding {@link android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB } before the Call object
+ * is passed to all InCallServices is crucial for InCallServices that may disrupt the user and
+ * potentially bypass the current Do Not Disturb settings.
+ */
+public class DndCallFilter extends CallFilter {
+
+ private final Call mCall;
+ private final Ringer mRinger;
+
+ public DndCallFilter(Call call, Ringer ringer) {
+ mCall = call;
+ mRinger = ringer;
+ }
+
+ @VisibleForTesting
+ @Override
+ public CompletionStage<CallFilteringResult> startFilterLookup(CallFilteringResult result) {
+ CompletableFuture<CallFilteringResult> resultFuture = new CompletableFuture<>();
+
+ // start timer for query to NotificationManager
+ Log.addEvent(mCall, LogUtils.Events.DND_PRE_CHECK_INITIATED);
+
+ // query NotificationManager to determine if the call should ring or be suppressed
+ boolean shouldSuppress = !mRinger.shouldRingForContact(mCall);
+
+ // end timer
+ Log.addEvent(mCall, LogUtils.Events.DND_PRE_CHECK_COMPLETED, shouldSuppress);
+
+ // complete the resultFuture object
+ resultFuture.complete(new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true)
+ .setDndSuppressed(shouldSuppress)
+ .build());
+
+ return resultFuture;
+ }
+
+}
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
index 9fa864e60..d79e80ea6 100644
--- a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
@@ -41,6 +41,7 @@ public class IncomingCallFilterGraph {
.setShouldReject(false)
.setShouldAddToCallLog(true)
.setShouldShowNotification(true)
+ .setDndSuppressed(false)
.build();
private final CallFilterResultCallback mListener;
@@ -143,6 +144,7 @@ public class IncomingCallFilterGraph {
.setShouldSilence(false)
.setShouldAddToCallLog(true)
.setShouldShowNotification(true)
+ .setDndSuppressed(false)
.build();
for (CallFilter dependencyFilter : filter.getDependencies()) {
result = result.combine(dependencyFilter.getResult());
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
index 746965092..05e73d544 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -74,7 +74,7 @@ public class CallRedirectionProcessor implements CallRedirectionCallback {
mServiceType = serviceType;
}
- private void process() {
+ private void process(UserHandle userHandleForCallRedirection) {
Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE)
.setComponent(mComponentName);
ServiceConnection connection = new CallRedirectionServiceConnection();
@@ -83,7 +83,7 @@ public class CallRedirectionProcessor implements CallRedirectionCallback {
connection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
- UserHandle.CURRENT)) {
+ userHandleForCallRedirection)) {
Log.d(this, "bindService, found " + mServiceType + " call redirection service,"
+ " waiting for it to connect");
mConnection = connection;
@@ -175,6 +175,20 @@ public class CallRedirectionProcessor implements CallRedirectionCallback {
Log.endSession();
}
}
+
+ @Override
+ public void onBindingDied(ComponentName componentName) {
+ // Make sure we unbind the service if binding died to avoid background stating
+ // activity leaks
+ Log.startSession("CRSC.oBD");
+ try {
+ synchronized (mTelecomLock) {
+ finishCallRedirection();
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
}
private class CallRedirectionAdapter extends ICallRedirectionAdapter.Stub {
@@ -340,7 +354,8 @@ public class CallRedirectionProcessor implements CallRedirectionCallback {
mPhoneAccountHandle, mRedirectionGatewayInfo, mSpeakerphoneOn,
mVideoState, mShouldCancelCall, mUiAction);
} else {
- performCarrierCallRedirection();
+ // Use the current user for carrier call redirection
+ performCarrierCallRedirection(UserHandle.CURRENT);
}
} else if (mIsCarrierRedirectionPending) {
Log.addEvent(mCall, LogUtils.Events.REDIRECTION_COMPLETED_CARRIER);
@@ -356,39 +371,41 @@ public class CallRedirectionProcessor implements CallRedirectionCallback {
/**
* The entry to perform call redirection of the call from (@link CallsManager)
*/
- public void performCallRedirection() {
+ public void performCallRedirection(UserHandle userHandleForCallRedirection) {
// If the Gateway Info is set with intent, only request with carrier call redirection.
if (mRedirectionGatewayInfo != null) {
- performCarrierCallRedirection();
+ // Use the current user for carrier call redirection
+ performCarrierCallRedirection(UserHandle.CURRENT);
} else {
- performUserDefinedCallRedirection();
+ performUserDefinedCallRedirection(userHandleForCallRedirection);
}
}
- private void performUserDefinedCallRedirection() {
+ private void performUserDefinedCallRedirection(UserHandle userHandleForCallRedirection) {
Log.d(this, "performUserDefinedCallRedirection");
ComponentName componentName =
- mCallRedirectionProcessorHelper.getUserDefinedCallRedirectionService();
+ mCallRedirectionProcessorHelper.
+ getUserDefinedCallRedirectionService(userHandleForCallRedirection);
if (componentName != null) {
mAttempt = new CallRedirectionAttempt(componentName, SERVICE_TYPE_USER_DEFINED);
- mAttempt.process();
+ mAttempt.process(userHandleForCallRedirection);
mIsUserDefinedRedirectionPending = true;
processTimeoutForCallRedirection(SERVICE_TYPE_USER_DEFINED);
} else {
Log.i(this, "There are no user-defined call redirection services installed on this"
+ " device.");
- performCarrierCallRedirection();
+ performCarrierCallRedirection(UserHandle.CURRENT);
}
}
- private void performCarrierCallRedirection() {
+ private void performCarrierCallRedirection(UserHandle userHandleForCallRedirection) {
Log.d(this, "performCarrierCallRedirection");
ComponentName componentName =
mCallRedirectionProcessorHelper.getCarrierCallRedirectionService(
mPhoneAccountHandle);
if (componentName != null) {
mAttempt = new CallRedirectionAttempt(componentName, SERVICE_TYPE_CARRIER);
- mAttempt.process();
+ mAttempt.process(userHandleForCallRedirection);
mIsCarrierRedirectionPending = true;
processTimeoutForCallRedirection(SERVICE_TYPE_CARRIER);
} else {
@@ -429,18 +446,22 @@ public class CallRedirectionProcessor implements CallRedirectionCallback {
}
/**
- * Checks if Telecom can make call redirection with any available call redirection service.
+ * Checks if Telecom can make call redirection with any available call redirection service
+ * as the specified user.
*
* @return {@code true} if it can; {@code false} otherwise.
*/
- public boolean canMakeCallRedirectionWithService() {
- boolean canMakeCallRedirectionWithService =
- mCallRedirectionProcessorHelper.getUserDefinedCallRedirectionService() != null
+ public boolean canMakeCallRedirectionWithServiceAsUser(
+ UserHandle userHandleForCallRedirection) {
+ boolean canMakeCallRedirectionWithServiceAsUser =
+ mCallRedirectionProcessorHelper
+ .getUserDefinedCallRedirectionService(userHandleForCallRedirection) != null
|| mCallRedirectionProcessorHelper.getCarrierCallRedirectionService(
mPhoneAccountHandle) != null;
- Log.i(this, "Can make call redirection with any available service: "
- + canMakeCallRedirectionWithService);
- return canMakeCallRedirectionWithService;
+ Log.i(this, "Can make call redirection with any "
+ + "available service as user (" + userHandleForCallRedirection
+ + ") : " + canMakeCallRedirectionWithServiceAsUser);
+ return canMakeCallRedirectionWithServiceAsUser;
}
/**
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
index 9771d65d4..7a2d4154d 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
@@ -23,6 +23,7 @@ import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.PersistableBundle;
+import android.os.UserHandle;
import android.telecom.CallRedirectionService;
import android.telecom.GatewayInfo;
import android.telecom.Log;
@@ -53,8 +54,9 @@ public class CallRedirectionProcessorHelper {
}
@VisibleForTesting
- public ComponentName getUserDefinedCallRedirectionService() {
- String packageName = mCallsManager.getRoleManagerAdapter().getDefaultCallRedirectionApp();
+ public ComponentName getUserDefinedCallRedirectionService(UserHandle userHandle) {
+ String packageName = mCallsManager.getRoleManagerAdapter().getDefaultCallRedirectionApp(
+ userHandle);
if (TextUtils.isEmpty(packageName)) {
Log.i(this, "PackageName is empty. Not performing user-defined call redirection.");
return null;
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 9ad0da457..ef85fc736 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -26,9 +26,11 @@ import android.os.IBinder;
import android.os.PowerManager;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.provider.BlockedNumberContract;
import android.telecom.Log;
import android.telecom.CallerInfoAsyncQuery;
+import android.view.accessibility.AccessibilityManager;
import com.android.internal.telecom.IInternalServiceRetriever;
import com.android.internal.telecom.ITelecomLoader;
@@ -53,14 +55,19 @@ import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
import com.android.server.telecom.ProximitySensorManagerFactory;
import com.android.server.telecom.InCallWakeLockController;
import com.android.server.telecom.ProximitySensorManager;
+import com.android.server.telecom.Ringer;
import com.android.server.telecom.RoleManagerAdapterImpl;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.TelecomWakeLock;
import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.settings.BlockedNumbersUtil;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.MissedCallNotifierImpl;
import com.android.server.telecom.ui.NotificationChannelManager;
+import java.util.concurrent.Executors;
+
/**
* Implementation of the ITelecom interface.
*/
@@ -154,7 +161,7 @@ public class TelecomService extends Service implements TelecomSystem.Component {
new InCallWakeLockControllerFactory() {
@Override
public InCallWakeLockController create(Context context,
- CallsManager callsManager) {
+ CallsManager callsManager) {
return new InCallWakeLockController(
new TelecomWakeLock(context,
PowerManager.FULL_WAKE_LOCK,
@@ -191,7 +198,38 @@ public class TelecomService extends Service implements TelecomSystem.Component {
new RoleManagerAdapterImpl(context,
(RoleManager) context.getSystemService(Context.ROLE_SERVICE)),
new ContactsAsyncHelper.Factory(),
- internalServiceRetriever.getDeviceIdleController()));
+ internalServiceRetriever.getDeviceIdleController(),
+ new Ringer.AccessibilityManagerAdapter() {
+ @Override
+ public boolean startFlashNotificationSequence(
+ @androidx.annotation.NonNull Context context, int reason) {
+ return context.getSystemService(AccessibilityManager.class)
+ .startFlashNotificationSequence(context, reason);
+ }
+
+ @Override
+ public boolean stopFlashNotificationSequence(
+ @androidx.annotation.NonNull Context context) {
+ return context.getSystemService(AccessibilityManager.class)
+ .stopFlashNotificationSequence(context);
+ }
+ },
+ Executors.newCachedThreadPool(),
+ new BlockedNumbersAdapter() {
+ @Override
+ public boolean shouldShowEmergencyCallNotification(Context
+ context) {
+ return BlockedNumberContract.SystemContract
+ .shouldShowEmergencyCallNotification(context);
+ }
+
+ @Override
+ public void updateEmergencyCallNotification(Context context,
+ boolean showNotification) {
+ BlockedNumbersUtil.updateEmergencyCallNotification(context,
+ showNotification);
+ }
+ }));
}
}
diff --git a/src/com/android/server/telecom/components/UserCallActivity.java b/src/com/android/server/telecom/components/UserCallActivity.java
index 1d85884a6..d7b2001c1 100644
--- a/src/com/android/server/telecom/components/UserCallActivity.java
+++ b/src/com/android/server/telecom/components/UserCallActivity.java
@@ -74,7 +74,8 @@ public class UserCallActivity extends Activity implements TelecomSystem.Componen
// ActivityThread.ActivityClientRecord#intent directly.
// Modifying directly may be a potential risk when relaunching this activity.
new UserCallIntentProcessor(this, userHandle).processIntent(new Intent(intent),
- getCallingPackage(), true /* hasCallAppOp*/, false /* isLocalInvocation */);
+ 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 cad7b4c67..a4602c1ca 100755
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -69,6 +69,7 @@ public class UserCallIntentProcessor {
*
* @param intent The intent.
* @param callingPackageName The package name of the calling app.
+ * @param isSelfManaged {@code true} if SelfManaged profile enabled.
* @param canCallNonEmergency {@code true} if the caller is permitted to call non-emergency
* numbers.
* @param isLocalInvocation {@code true} if the caller is within the system service (i.e. the
@@ -79,19 +80,21 @@ public class UserCallIntentProcessor {
* service resides.
*/
public void processIntent(Intent intent, String callingPackageName,
- boolean canCallNonEmergency, boolean isLocalInvocation) {
+ boolean isSelfManaged, boolean canCallNonEmergency,
+ boolean isLocalInvocation) {
String action = intent.getAction();
if (Intent.ACTION_CALL.equals(action) ||
Intent.ACTION_CALL_PRIVILEGED.equals(action) ||
Intent.ACTION_CALL_EMERGENCY.equals(action)) {
- processOutgoingCallIntent(intent, callingPackageName, canCallNonEmergency,
- isLocalInvocation);
+ processOutgoingCallIntent(intent, callingPackageName, isSelfManaged,
+ canCallNonEmergency, isLocalInvocation);
}
}
private void processOutgoingCallIntent(Intent intent, String callingPackageName,
- boolean canCallNonEmergency, boolean isLocalInvocation) {
+ boolean isSelfManaged, boolean canCallNonEmergency,
+ boolean isLocalInvocation) {
Uri handle = intent.getData();
if (handle == null) return;
String scheme = handle.getScheme();
@@ -102,40 +105,43 @@ public class UserCallIntentProcessor {
handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null);
}
- // 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) {
+ 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;
}
- final Intent adminSupportIntent = dpm.createAdminSupportIntent(
- UserManager.DISALLOW_OUTGOING_CALLS);
- if (adminSupportIntent != null) {
- mContext.startActivity(adminSupportIntent);
- }
- return;
}
}
}
- if (!canCallNonEmergency && !TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
+ 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 "
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index bc54e110a..5fa5f06af 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -40,7 +40,6 @@ import android.telephony.TelephonyManager;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@@ -101,6 +100,8 @@ public class BlockedNumbersActivity extends ListActivity
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
+ // set the talkback voice prompt to "Back" instead of "Navigate Up"
+ actionBar.setHomeActionContentDescription(R.string.back);
}
if (!BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
@@ -153,8 +154,11 @@ public class BlockedNumbersActivity extends ListActivity
updateButterBar();
}
};
- registerReceiver(mBlockingStatusReceiver, new IntentFilter(
- BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED));
+ IntentFilter blockStatusIntentFilter = new IntentFilter(
+ BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+ blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
+ Context.RECEIVER_EXPORTED);
getLoaderManager().initLoader(0, null, this);
}
diff --git a/src/com/android/server/telecom/stats/CallFailureCause.java b/src/com/android/server/telecom/stats/CallFailureCause.java
new file mode 100644
index 000000000..3eb2321f6
--- /dev/null
+++ b/src/com/android/server/telecom/stats/CallFailureCause.java
@@ -0,0 +1,69 @@
+/*
+ * 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.stats;
+
+/**
+ * Indicating the failure reason why a new call cannot be made.
+ * The codes are synced with CallFailureCauseEnum defined in enums.proto.
+ */
+public enum CallFailureCause {
+ /** The call is normally started. */
+ NONE(0),
+ /** Necessary parameters are invalid or null. */
+ INVALID_USE(1),
+ /** There is an emergency call ongoing. */
+ IN_EMERGENCY_CALL(2),
+ /** There is an live call that cannot be held. */
+ CANNOT_HOLD_CALL(3),
+ /** There are maximum number of outgoing calls already. */
+ MAX_OUTGOING_CALLS(4),
+ /** There are maximum number of ringing calls already. */
+ MAX_RINGING_CALLS(5),
+ /** There are maximum number of calls in hold already. */
+ MAX_HOLD_CALLS(6),
+ /* There are maximum number of self-managed calls already. */
+ MAX_SELF_MANAGED_CALLS(7);
+
+ private final int mCode;
+
+ /**
+ * Creates a new CallFailureCause.
+ *
+ * @param code The code for the failure cause.
+ */
+ CallFailureCause(int code) {
+ mCode = code;
+ }
+
+ /**
+ * Returns the code for the failure.
+ *
+ * @return The code for the failure cause.
+ */
+ public int getCode() {
+ return mCode;
+ }
+
+ /**
+ * Check if this enum represents a non-failure case.
+ *
+ * @return True if success.
+ */
+ public boolean isSuccess() {
+ return this == NONE;
+ }
+}
diff --git a/src/com/android/server/telecom/stats/CallStateChangedAtomWriter.java b/src/com/android/server/telecom/stats/CallStateChangedAtomWriter.java
new file mode 100644
index 000000000..55b002e25
--- /dev/null
+++ b/src/com/android/server/telecom/stats/CallStateChangedAtomWriter.java
@@ -0,0 +1,119 @@
+/*
+ * 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.stats;
+
+import android.content.pm.PackageManager;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.TelecomStatsLog;
+
+/**
+ * Collects and stores data for CallStateChanged atom for each call, and provide a
+ * method to write the data to statsd whenever the call state changes.
+ */
+public class CallStateChangedAtomWriter {
+ private boolean mIsSelfManaged = false;
+ private boolean mIsExternalCall = false;
+ private boolean mIsEmergencyCall = false;
+ private int mUid = -1;
+ private int mDurationSeconds = 0;
+ private int mExistingCallCount = 0;
+ private int mHeldCallCount = 0;
+ private CallFailureCause mStartFailCause = CallFailureCause.NONE;
+ private DisconnectCause mDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
+
+ /**
+ * Write collected data and current call state to statsd.
+ *
+ * @param state Current call state.
+ */
+ public void write(int state) {
+ TelecomStatsLog.write(TelecomStatsLog.CALL_STATE_CHANGED,
+ state,
+ state == CallState.DISCONNECTED ?
+ mDisconnectCause.getCode() : DisconnectCause.UNKNOWN,
+ mIsSelfManaged,
+ mIsExternalCall,
+ mIsEmergencyCall,
+ mUid,
+ state == CallState.DISCONNECTED ? mDurationSeconds : 0,
+ mExistingCallCount,
+ mHeldCallCount,
+ state == CallState.DISCONNECTED ?
+ mStartFailCause.getCode() : CallFailureCause.NONE.getCode());
+ }
+
+ public CallStateChangedAtomWriter setSelfManaged(boolean isSelfManaged) {
+ mIsSelfManaged = isSelfManaged;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setExternalCall(boolean isExternalCall) {
+ mIsExternalCall = isExternalCall;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setEmergencyCall(boolean isEmergencyCall) {
+ mIsEmergencyCall = isEmergencyCall;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setUid(int uid) {
+ mUid = uid;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setUid(String packageName, PackageManager pm) {
+ try {
+ final int uid = pm.getPackageUid(packageName, PackageManager.PackageInfoFlags.of(0));
+ return setUid(uid);
+
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(this, e, "Could not find the package");
+ }
+ return setUid(-1);
+ }
+
+ public CallStateChangedAtomWriter setDurationSeconds(int duration) {
+ if (duration >= 0) {
+ mDurationSeconds = duration;
+ }
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setExistingCallCount(int count) {
+ mExistingCallCount = count;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter increaseHeldCallCount() {
+ mHeldCallCount++;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setDisconnectCause(DisconnectCause cause) {
+ mDisconnectCause = cause;
+ return this;
+ }
+
+ public CallStateChangedAtomWriter setStartFailCause(CallFailureCause cause) {
+ mStartFailCause = cause;
+ return this;
+ }
+}
diff --git a/src/com/android/server/telecom/ui/AudioProcessingNotification.java b/src/com/android/server/telecom/ui/AudioProcessingNotification.java
index 7a6146025..952bee8fc 100644
--- a/src/com/android/server/telecom/ui/AudioProcessingNotification.java
+++ b/src/com/android/server/telecom/ui/AudioProcessingNotification.java
@@ -19,6 +19,7 @@ package com.android.server.telecom.ui;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
+import android.os.UserHandle;
import android.telecom.Log;
import com.android.server.telecom.Call;
@@ -52,7 +53,8 @@ public class AudioProcessingNotification extends CallsManagerListenerBase {
showAudioProcessingNotification(call);
} else if (oldState == CallState.AUDIO_PROCESSING
&& newState != CallState.AUDIO_PROCESSING) {
- cancelAudioProcessingNotification();
+ cancelAudioProcessingNotification(
+ call.getAssociatedUser());
}
}
@@ -66,7 +68,8 @@ public class AudioProcessingNotification extends CallsManagerListenerBase {
@Override
public void onCallRemoved(Call call) {
if (call == mCallInAudioProcessing) {
- cancelAudioProcessingNotification();
+ cancelAudioProcessingNotification(
+ call.getAssociatedUser());
}
}
@@ -76,7 +79,8 @@ public class AudioProcessingNotification extends CallsManagerListenerBase {
* @param call The missed call.
*/
private void showAudioProcessingNotification(Call call) {
- Log.i(this, "showAudioProcessingNotification");
+ Log.i(this, "showAudioProcessingNotification for user = %s",
+ call.getAssociatedUser());
mCallInAudioProcessing = call;
Notification.Builder builder = new Notification.Builder(mContext,
@@ -92,12 +96,14 @@ public class AudioProcessingNotification extends CallsManagerListenerBase {
Notification notification = builder.build();
- mNotificationManager.notify(
- NOTIFICATION_TAG, AUDIO_PROCESSING_NOTIFICATION_ID, notification);
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG, AUDIO_PROCESSING_NOTIFICATION_ID,
+ notification, mCallInAudioProcessing.getAssociatedUser());
}
/** Cancels the audio processing notification. */
- private void cancelAudioProcessingNotification() {
- mNotificationManager.cancel(NOTIFICATION_TAG, AUDIO_PROCESSING_NOTIFICATION_ID);
+ private void cancelAudioProcessingNotification(UserHandle userHandle) {
+ Log.i(this, "cancelAudioProcessingNotification for user = %s", userHandle);
+ mNotificationManager.cancelAsUser(NOTIFICATION_TAG,
+ AUDIO_PROCESSING_NOTIFICATION_ID, userHandle);
}
}
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
new file mode 100644
index 000000000..8414047a2
--- /dev/null
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -0,0 +1,324 @@
+/*
+ * 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.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.telecom.AppLabelProxy;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Class responsible for tracking if there is a call which is being streamed and posting a
+ * notification which informs the user that a call is streaming. The user has two possible actions:
+ * disconnect the call, bring the call back to the current device (stop streaming).
+ */
+public class CallStreamingNotification extends CallsManagerListenerBase implements Call.Listener {
+ // URI scheme used for data related to the notification actions.
+ public static final String CALL_ID_SCHEME = "callid";
+ // The default streaming notification ID.
+ private static final int STREAMING_NOTIFICATION_ID = 90210;
+ // Tag for streaming notification.
+ private static final String NOTIFICATION_TAG =
+ CallStreamingNotification.class.getSimpleName();
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ // Used to get the app name for the notification.
+ private final AppLabelProxy mAppLabelProxy;
+ // An executor that can be used to fire off async tasks that do not block Telecom in any manner.
+ private final Executor mAsyncTaskExecutor;
+ // The call which is streaming.
+ private Call mStreamingCall;
+ // Lock for notification post/remove -- these happen outside the Telecom sync lock.
+ private final Object mNotificationLock = new Object();
+
+ // Whether the notification is showing.
+ @GuardedBy("mNotificationLock")
+ private boolean mIsNotificationShowing = false;
+ @GuardedBy("mNotificationLock")
+ private UserHandle mNotificationUserHandle;
+
+ public CallStreamingNotification(@NonNull Context context,
+ @NonNull AppLabelProxy appLabelProxy,
+ @NonNull Executor asyncTaskExecutor) {
+ mContext = context;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ mAppLabelProxy = appLabelProxy;
+ mAsyncTaskExecutor = asyncTaskExecutor;
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (call.isStreaming()) {
+ trackStreamingCall(call);
+ enqueueStreamingNotification(call);
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (call == mStreamingCall) {
+ trackStreamingCall(null);
+ dequeueStreamingNotification();
+ }
+ }
+
+ /**
+ * Handles streaming state changes for a call.
+ * @param call the call
+ * @param isStreaming whether it is streaming or not
+ */
+ @Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ Log.i(this, "onCallStreamingStateChanged: call=%s, isStreaming=%b", call.getId(),
+ isStreaming);
+
+ if (isStreaming) {
+ trackStreamingCall(call);
+ enqueueStreamingNotification(call);
+ } else {
+ trackStreamingCall(null);
+ dequeueStreamingNotification();
+ }
+ }
+
+ /**
+ * Change the streaming call we are tracking.
+ * @param call the call.
+ */
+ private void trackStreamingCall(Call call) {
+ if (mStreamingCall != null) {
+ mStreamingCall.removeListener(this);
+ }
+ mStreamingCall = call;
+ if (mStreamingCall != null) {
+ mStreamingCall.addListener(this);
+ }
+ }
+
+ /**
+ * Enqueue an async task to post/repost the streaming notification.
+ * Note: This happens INSIDE the telecom lock.
+ * @param call the call to post notification for.
+ */
+ private void enqueueStreamingNotification(Call call) {
+ final Bitmap contactPhotoBitmap = call.getPhotoIcon();
+ mAsyncTaskExecutor.execute(() -> {
+ Icon contactPhotoIcon = null;
+ try {
+ contactPhotoIcon = Icon.createWithResource(mContext.getResources(),
+ R.drawable.person_circle);
+ } catch (Exception e) {
+ // All loads of things can do wrong when working with bitmaps and images, so to
+ // ensure Telecom doesn't crash, lets try/catch to be sure.
+ Log.e(this, e, "enqueueStreamingNotification: Couldn't build avatar icon");
+ }
+ showStreamingNotification(call.getId(),
+ call.getAssociatedUser(), call.getCallerDisplayName(),
+ call.getHandle(), contactPhotoIcon,
+ call.getTargetPhoneAccount().getComponentName().getPackageName(),
+ call.getConnectTimeMillis());
+ });
+ }
+
+ /**
+ * Dequeues the call streaming notification.
+ * Note: This is yo be called within the Telecom sync lock to launch the task to remove the call
+ * streaming notification.
+ */
+ private void dequeueStreamingNotification() {
+ mAsyncTaskExecutor.execute(() -> hideStreamingNotification());
+ }
+
+ /**
+ * Show the call streaming notification. This is intended to run outside the Telecom sync lock.
+ *
+ * @param callId the call ID we're streaming.
+ * @param userHandle the userhandle for the call.
+ * @param callerName the name of the caller/callee associated with the call
+ * @param callerAddress the address associated with the caller/callee
+ * @param photoIcon the contact photo icon if available
+ * @param appPackageName the package name for the app to post the notification for
+ * @param connectTimeMillis when the call connected (for chronometer in the notification)
+ */
+ private void showStreamingNotification(final String callId, final UserHandle userHandle,
+ String callerName, Uri callerAddress, Icon photoIcon, String appPackageName,
+ long connectTimeMillis) {
+ Log.i(this, "showStreamingNotification; callid=%s, hasPhoto=%b", callId, photoIcon != null);
+
+ // 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();
+ }
+
+ // Action to hangup; this can use the default hangup action from the call style
+ // notification.
+ Intent hangupIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_HANGUP_CALL,
+ Uri.fromParts(CALL_ID_SCHEME, callId, null),
+ mContext, TelecomBroadcastReceiver.class);
+ PendingIntent hangupPendingIntent = PendingIntent.getBroadcast(mContext, 0, hangupIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ // Action to switch here.
+ Intent switchHereIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_STOP_STREAMING,
+ Uri.fromParts(CALL_ID_SCHEME, callId, null),
+ mContext, TelecomBroadcastReceiver.class);
+ PendingIntent switchHerePendingIntent = PendingIntent.getBroadcast(mContext, 0,
+ switchHereIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ // Apply a span to the string to colorize it using the "answer" color.
+ Spannable spannable = new SpannableString(
+ mContext.getString(R.string.call_streaming_notification_action_switch_here));
+ spannable.setSpan(new ForegroundColorSpan(
+ com.android.internal.R.color.call_notification_answer_color), 0, spannable.length(),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ // Use the "phone link" icon per mock.
+ Icon switchHereIcon = Icon.createWithResource(mContext, R.drawable.gm_phonelink);
+ Notification.Action.Builder switchHereBuilder = new Notification.Action.Builder(
+ switchHereIcon,
+ spannable,
+ switchHerePendingIntent);
+ Notification.Action switchHereAction = switchHereBuilder.build();
+
+ // Notifications use a "person" entity to identify caller/callee.
+ Person.Builder personBuilder = new Person.Builder()
+ .setName(callerName);
+
+ // Some apps use phone numbers to identify; these are something the notification framework
+ // can lookup in contacts to provide more data
+ if (callerAddress != null && PhoneAccount.SCHEME_TEL.equals(callerAddress)) {
+ personBuilder.setUri(callerAddress.toString());
+ }
+ if (photoIcon != null) {
+ personBuilder.setIcon(photoIcon);
+ }
+ Person person = personBuilder.build();
+
+ // Call Style notification requires a full screen intent, so we'll just link in a null
+ // pending intent
+ Intent nullIntent = new Intent();
+ PendingIntent nullPendingIntent = PendingIntent.getBroadcast(mContext, 0, nullIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ Notification.Builder builder = new Notification.Builder(mContext,
+ NotificationChannelManager.CHANNEL_ID_CALL_STREAMING)
+ // Use call style to get the general look and feel for the notification; it provides
+ // a hangup action with the right action already so we can leverage that. The
+ // "switch here" action will be a custom action defined later.
+ .setStyle(Notification.CallStyle.forOngoingCall(person, hangupPendingIntent))
+ .setSmallIcon(R.drawable.ic_phone)
+ .setContentText(mContext.getString(
+ R.string.call_streaming_notification_body))
+ // Report call time
+ .setWhen(connectTimeMillis)
+ .setShowWhen(true)
+ .setUsesChronometer(true)
+ // Set the full screen intent; this is just tricking notification manager into
+ // letting us use this style. Sssh.
+ .setFullScreenIntent(nullPendingIntent, true)
+ .setColorized(true)
+ .addAction(switchHereAction);
+ Notification notification = builder.build();
+
+ synchronized(mNotificationLock) {
+ mIsNotificationShowing = true;
+ mNotificationUserHandle = userHandle;
+ try {
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG, STREAMING_NOTIFICATION_ID,
+ notification, userHandle);
+ } catch (Exception e) {
+ // We don't want to crash Telecom if something changes with the requirements for the
+ // notification.
+ Log.e(this, e, "Notification post failed.");
+ }
+ }
+ }
+
+ /**
+ * Removes the posted streaming notification. Intended to run outside the telecom lock.
+ */
+ private void hideStreamingNotification() {
+ Log.i(this, "hideStreamingNotification");
+ synchronized(mNotificationLock) {
+ if (mIsNotificationShowing) {
+ mIsNotificationShowing = false;
+ mNotificationManager.cancelAsUser(NOTIFICATION_TAG,
+ STREAMING_NOTIFICATION_ID, mNotificationUserHandle);
+ }
+ }
+ }
+
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable, int width, int height) {
+ if (drawable == null) {
+ return null;
+ }
+
+ Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ if (width > 0 || height > 0) {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ } else if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+ // Needed for drawables that are just a colour.
+ bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ } else {
+ bitmap =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
index 66f9fe49d..160428585 100644
--- a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
+++ b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
@@ -143,8 +143,7 @@ public class DisconnectedCallNotifier extends CallsManagerListenerBase {
DisconnectCause.REASON_EMERGENCY_CALL_PLACED.equals(cause.getReason())) {
// Clear any existing notification.
clearNotification(mCallsManager.getCurrentUserHandle());
- UserHandle userHandle = call.getTargetPhoneAccount() != null ?
- call.getTargetPhoneAccount().getUserHandle() : call.getInitiatingUser();
+ UserHandle userHandle = call.getAssociatedUser();
// As a last resort, use the current user to display the notification.
if (userHandle == null) userHandle = mCallsManager.getCurrentUserHandle();
mPendingCallNotification = new CallInfo(userHandle, call.getHandle(),
diff --git a/src/com/android/server/telecom/ui/IncomingCallNotifier.java b/src/com/android/server/telecom/ui/IncomingCallNotifier.java
index 0c1c5a3d9..d419163c2 100644
--- a/src/com/android/server/telecom/ui/IncomingCallNotifier.java
+++ b/src/com/android/server/telecom/ui/IncomingCallNotifier.java
@@ -22,6 +22,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.UserHandle;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -166,22 +167,26 @@ public class IncomingCallNotifier extends CallsManagerListenerBase {
showIncomingCallNotification(mIncomingCall);
} else if (hadIncomingCall && !hasIncomingCall) {
previousIncomingCall.removeListener(mCallListener);
- hideIncomingCallNotification();
+ hideIncomingCallNotification(
+ previousIncomingCall.getAssociatedUser());
}
}
}
private void showIncomingCallNotification(Call call) {
- Log.i(this, "showIncomingCallNotification showCall = %s", call);
+ Log.i(this, "showIncomingCallNotification showCall = %s for user = %s",
+ call, call.getAssociatedUser());
Notification.Builder builder = getNotificationBuilder(call,
mCallsManagerProxy.getActiveCall());
- mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL, builder.build());
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL,
+ builder.build(), call.getAssociatedUser());
}
- private void hideIncomingCallNotification() {
- Log.i(this, "hideIncomingCallNotification");
- mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL);
+ private void hideIncomingCallNotification(UserHandle userHandle) {
+ Log.i(this, "hideIncomingCallNotification for user = %s", userHandle);
+ mNotificationManager.cancelAsUser(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL,
+ userHandle);
}
private String getNotificationName(Call call) {
diff --git a/src/com/android/server/telecom/ui/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index 58794a602..b3cb2c3c4 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -40,6 +40,7 @@ public class NotificationChannelManager {
public static final String CHANNEL_ID_AUDIO_PROCESSING = "TelecomBackgroundAudioProcessing";
public static final String CHANNEL_ID_DISCONNECTED_CALLS = "TelecomDisconnectedCalls";
public static final String CHANNEL_ID_IN_CALL_SERVICE_CRASH = "TelecomInCallServiceCrash";
+ public static final String CHANNEL_ID_CALL_STREAMING = "TelecomCallStreaming";
private BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
@Override
@@ -50,8 +51,9 @@ public class NotificationChannelManager {
};
public void createChannels(Context context) {
- context.registerReceiver(mLocaleChangeReceiver,
- new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
+ IntentFilter localeChangedfilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ localeChangedfilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ context.registerReceiver(mLocaleChangeReceiver, localeChangedfilter);
createOrUpdateAll(context);
}
@@ -63,6 +65,7 @@ public class NotificationChannelManager {
createOrUpdateChannel(context, CHANNEL_ID_AUDIO_PROCESSING);
createOrUpdateChannel(context, CHANNEL_ID_DISCONNECTED_CALLS);
createOrUpdateChannel(context, CHANNEL_ID_IN_CALL_SERVICE_CRASH);
+ createOrUpdateChannel(context, CHANNEL_ID_CALL_STREAMING);
}
private void createOrUpdateChannel(Context context, String channelId) {
@@ -127,6 +130,14 @@ public class NotificationChannelManager {
lights = true;
vibration = true;
sound = null;
+ 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/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
new file mode 100644
index 000000000..93d98363b
--- /dev/null
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -0,0 +1,144 @@
+/*
+ * 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.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.DisconnectCause;
+import android.util.Log;
+
+import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.TransactionalServiceWrapper;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 {
+ private static final String TAG = CallEventCallbackAckTransaction.class.getSimpleName();
+ private final ICallEventCallback mICallEventCallback;
+ private final String mAction;
+ private final String mCallId;
+ // optional values
+ private int mVideoState = CallAttributes.AUDIO_CALL;
+ private DisconnectCause mDisconnectCause = null;
+
+ private final VoipCallTransactionResult TRANSACTION_FAILED = new VoipCallTransactionResult(
+ CODE_OPERATION_TIMED_OUT, "failed to complete the operation before timeout");
+
+ private static class AckResultReceiver extends ResultReceiver {
+ CountDownLatch mCountDownLatch;
+
+ public AckResultReceiver(CountDownLatch latch) {
+ super(null);
+ mCountDownLatch = latch;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
+ mCountDownLatch.countDown();
+ }
+ }
+ }
+
+ public CallEventCallbackAckTransaction(ICallEventCallback service, String action,
+ String callId, TelecomSystem.SyncRoot lock) {
+ super(lock);
+ mICallEventCallback = service;
+ mAction = action;
+ mCallId = callId;
+ }
+
+
+ public CallEventCallbackAckTransaction(ICallEventCallback service, String action, String callId,
+ int videoState, TelecomSystem.SyncRoot lock) {
+ super(lock);
+ mICallEventCallback = service;
+ mAction = action;
+ mCallId = callId;
+ mVideoState = videoState;
+ }
+
+ public CallEventCallbackAckTransaction(ICallEventCallback service, String action, String callId,
+ DisconnectCause cause, TelecomSystem.SyncRoot lock) {
+ super(lock);
+ mICallEventCallback = service;
+ mAction = action;
+ mCallId = callId;
+ mDisconnectCause = cause;
+ }
+
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CountDownLatch latch = new CountDownLatch(1);
+ ResultReceiver receiver = new AckResultReceiver(latch);
+
+ try {
+ switch (mAction) {
+ case TransactionalServiceWrapper.ON_SET_INACTIVE:
+ mICallEventCallback.onSetInactive(mCallId, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_DISCONNECT:
+ mICallEventCallback.onDisconnect(mCallId, mDisconnectCause, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_SET_ACTIVE:
+ mICallEventCallback.onSetActive(mCallId, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_ANSWER:
+ mICallEventCallback.onAnswer(mCallId, mVideoState, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_STREAMING_STARTED:
+ mICallEventCallback.onCallStreamingStarted(mCallId, receiver);
+ break;
+ }
+ } catch (RemoteException remoteException) {
+ return CompletableFuture.completedFuture(TRANSACTION_FAILED);
+ }
+
+ try {
+ // wait for the client to ack that CallEventCallback
+ boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+ if (!success) {
+ // client send onError and failed to complete transaction
+ Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
+ + " client failed to complete the [%s] transaction", mAction));
+ return CompletableFuture.completedFuture(TRANSACTION_FAILED);
+ } else {
+ // success
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ "success"));
+ }
+ } catch (InterruptedException ie) {
+ return CompletableFuture.completedFuture(TRANSACTION_FAILED);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/voip/EndCallTransaction.java b/src/com/android/server/telecom/voip/EndCallTransaction.java
new file mode 100644
index 000000000..0cb74581f
--- /dev/null
+++ b/src/com/android/server/telecom/voip/EndCallTransaction.java
@@ -0,0 +1,62 @@
+/*
+ * 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.telecom.DisconnectCause;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * This transaction should only be created for a CallControl action.
+ */
+public class EndCallTransaction extends VoipCallTransaction {
+ private static final String TAG = EndCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+ private DisconnectCause mCause;
+
+ public EndCallTransaction(CallsManager callsManager, DisconnectCause cause, Call call) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ mCause = cause;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ int code = mCause.getCode();
+ Log.d(TAG, String.format("processTransaction: mCode=[%d], mCall=[%s]", code, mCall));
+
+ if (mCall.getState() == CallState.RINGING && code == DisconnectCause.LOCAL) {
+ mCause = new DisconnectCause(DisconnectCause.REJECTED,
+ "overrode cause in EndCallTransaction");
+ }
+
+ mCallsManager.markCallAsDisconnected(mCall, mCause);
+ mCallsManager.markCallAsRemoved(mCall);
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ "EndCallTransaction: RESULT_SUCCEED"));
+ }
+}
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
new file mode 100644
index 000000000..e037a798c
--- /dev/null
+++ b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
@@ -0,0 +1,59 @@
+
+/*
+ * 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.Bundle;
+import android.os.ResultReceiver;
+import android.telecom.CallEndpoint;
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class EndpointChangeTransaction extends VoipCallTransaction {
+ private static final String TAG = EndpointChangeTransaction.class.getSimpleName();
+ private final CallEndpoint mCallEndpoint;
+ private final CallsManager mCallsManager;
+
+ public EndpointChangeTransaction(CallEndpoint endpoint, CallsManager callsManager) {
+ super(callsManager.getLock());
+ mCallEndpoint = endpoint;
+ mCallsManager = callsManager;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.i(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> 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));
+ } else {
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED, null));
+ }
+ }
+ });
+ return future;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/HoldCallTransaction.java b/src/com/android/server/telecom/voip/HoldCallTransaction.java
new file mode 100644
index 000000000..6c4e8b7cd
--- /dev/null
+++ b/src/com/android/server/telecom/voip/HoldCallTransaction.java
@@ -0,0 +1,56 @@
+/*
+ * 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.telecom.CallException;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class HoldCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = HoldCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+
+ public HoldCallTransaction(CallsManager callsManager, Call call) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ if (mCallsManager.canHold(mCall)) {
+ mCallsManager.markCallAsOnHold(mCall);
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ } else {
+ Log.d(TAG, "processTransaction: onError");
+ future.complete(new VoipCallTransactionResult(
+ 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/voip/IncomingCallTransaction.java
new file mode 100644
index 000000000..d35030c83
--- /dev/null
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -0,0 +1,89 @@
+/*
+ * 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.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
+
+import android.os.Bundle;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class IncomingCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = IncomingCallTransaction.class.getSimpleName();
+ private final String mCallId;
+ private final CallAttributes mCallAttributes;
+ private final CallsManager mCallsManager;
+ private final Bundle mExtras;
+
+ public IncomingCallTransaction(String callId, CallAttributes callAttributes,
+ CallsManager callsManager, Bundle extras) {
+ super(callsManager.getLock());
+ mExtras = extras;
+ mCallId = callId;
+ mCallAttributes = callAttributes;
+ mCallsManager = callsManager;
+ }
+
+ public IncomingCallTransaction(String callId, CallAttributes callAttributes,
+ CallsManager callsManager) {
+ this(callId, callAttributes, callsManager, new Bundle());
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+
+ if (mCallsManager.isIncomingCallPermitted(mCallAttributes.getPhoneAccountHandle())) {
+ Log.d(TAG, "processTransaction: incoming call permitted");
+
+ Call call = mCallsManager.processIncomingCallIntent(
+ mCallAttributes.getPhoneAccountHandle(),
+ generateExtras(mCallAttributes), false);
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, call, "success"));
+ } else {
+ Log.d(TAG, "processTransaction: incoming call is not permitted at this time");
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "incoming call not permitted at the current time"));
+ }
+ }
+
+ private 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());
+ mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+ mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+ callAttributes.getAddress());
+ return mExtras;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
new file mode 100644
index 000000000..a245c1c12
--- /dev/null
+++ b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
@@ -0,0 +1,64 @@
+/*
+ * 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.OutcomeReceiver;
+import android.telecom.CallException;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+
+ public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ Log.d(TAG, "processTransaction: onResult");
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.d(TAG, "processTransaction: onError");
+ future.complete(new VoipCallTransactionResult(
+ exception.getCode(), exception.getMessage()));
+ }
+ });
+
+ return future;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
new file mode 100644
index 000000000..b2625e69a
--- /dev/null
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -0,0 +1,133 @@
+/*
+ * 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.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 android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.telecom.CallAttributes;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LoggedHandlerExecutor;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class OutgoingCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = OutgoingCallTransaction.class.getSimpleName();
+ private final String mCallId;
+ private final Context mContext;
+ private final String mCallingPackage;
+ private final CallAttributes mCallAttributes;
+ private final CallsManager mCallsManager;
+ private final Bundle mExtras;
+
+ public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
+ CallsManager callsManager, Bundle extras) {
+ super(callsManager.getLock());
+ mCallId = callId;
+ mContext = context;
+ mCallAttributes = callAttributes;
+ mCallsManager = callsManager;
+ mExtras = extras;
+ mCallingPackage = mContext.getOpPackageName();
+ }
+
+ public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
+ CallsManager callsManager) {
+ this(callId, context, callAttributes, callsManager, new Bundle());
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+
+ final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
+ CALL_PRIVILEGED) == PackageManager.PERMISSION_GRANTED;
+
+ final Intent intent = new Intent(hasCallPrivilegedPermission ?
+ Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, mCallAttributes.getAddress());
+
+ if (mCallsManager.isOutgoingCallPermitted(mCallAttributes.getPhoneAccountHandle())) {
+ Log.d(TAG, "processTransaction: outgoing call permitted");
+
+ CompletableFuture<Call> callFuture =
+ mCallsManager.startOutgoingCall(mCallAttributes.getAddress(),
+ mCallAttributes.getPhoneAccountHandle(),
+ generateExtras(mCallAttributes),
+ mCallAttributes.getPhoneAccountHandle().getUserHandle(),
+ intent,
+ mCallingPackage);
+
+ if (callFuture == null) {
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "incoming call not permitted at the current time"));
+ }
+ CompletionStage<VoipCallTransactionResult> result = callFuture.thenComposeAsync(
+ (call) -> {
+
+ Log.d(TAG, "processTransaction: completing future");
+
+ if (call == null) {
+ Log.d(TAG, "processTransaction: call is null");
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ 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());
+ }
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED,
+ call, null));
+ }
+ , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
+
+ return result;
+ } else {
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "incoming call not permitted at the current time"));
+
+ }
+ }
+
+ private 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());
+ mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+ return mExtras;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
new file mode 100644
index 000000000..6176087eb
--- /dev/null
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -0,0 +1,109 @@
+/*
+ * 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/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
new file mode 100644
index 000000000..f586cc38b
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
@@ -0,0 +1,105 @@
+/*
+ * 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.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * This transaction should be created when a requesting call would like to go from a valid inactive
+ * state (ex. HELD, RINGING, DIALING) to ACTIVE.
+ *
+ * This class performs some pre-checks to spot a failure in requesting a new call focus and sends
+ * the official request to transition the requested call to ACTIVE.
+ *
+ * Note:
+ * - This Transaction is used for CallControl and CallEventCallbacks, do not put logic in the
+ * onResult/onError that pertains to one direction.
+ * - MaybeHoldCallForNewCallTransaction was performed before this so any potential active calls
+ * should be held now.
+ */
+public class RequestNewActiveCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = RequestNewActiveCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+
+ public RequestNewActiveCallTransaction(CallsManager callsManager, Call call) {
+ super(callsManager.getLock());
+ mCallsManager = callsManager;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+ int currentCallState = mCall.getState();
+
+ // certain calls cannot go active/answered (ex. disconnect calls, etc.)
+ if (!canBecomeNewCallFocus(currentCallState)) {
+ future.complete(new VoipCallTransactionResult(
+ 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(
+ CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
+ "Already an active call. Request hold on current active call."));
+ return future;
+ }
+
+ mCallsManager.requestNewCallFocusAndVerify(mCall, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ Log.d(TAG, "processTransaction: onResult");
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.d(TAG, "processTransaction: onError");
+ future.complete(new VoipCallTransactionResult(
+ exception.getCode(), exception.getMessage()));
+ }
+ });
+
+ return future;
+ }
+
+ private boolean isPriorityCallingState(int currentCallState) {
+ return ConnectionServiceFocusManager.PRIORITY_FOCUS_CALL_STATE.contains(currentCallState);
+ }
+
+ private boolean canBecomeNewCallFocus(int currentCallState) {
+ return isPriorityCallingState(currentCallState) || currentCallState == CallState.ON_HOLD;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
new file mode 100644
index 000000000..b35b471e2
--- /dev/null
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -0,0 +1,118 @@
+/*
+ * 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
new file mode 100644
index 000000000..228bdde7c
--- /dev/null
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -0,0 +1,130 @@
+/*
+ * 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/VoipCallMonitor.java b/src/com/android/server/telecom/voip/VoipCallMonitor.java
new file mode 100644
index 000000000..3779a6d50
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -0,0 +1,365 @@
+/*
+ * 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.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
+import android.app.Notification;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.telecom.Call;
+
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.TelecomSystem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+public class VoipCallMonitor extends CallsManagerListenerBase {
+
+ private final List<Call> mNotificationPendingCalls;
+ // Same notification may be passed as different object in onNotificationPosted and
+ // onNotificationRemoved. Use its string as key to cache ongoing notifications.
+ private final Map<NotificationInfo, Call> mNotificationInfoToCallMap;
+ private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+ private ActivityManagerInternal mActivityManagerInternal;
+ private final Map<PhoneAccountHandle, ServiceConnection> mServices;
+ private NotificationListenerService mNotificationListener;
+ private final Object mLock = new Object();
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+ private final Context mContext;
+ private List<NotificationInfo> mCachedNotifications;
+ private TelecomSystem.SyncRoot mSyncRoot;
+
+ public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+ mSyncRoot = lock;
+ mContext = context;
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mNotificationPendingCalls = new ArrayList<>();
+ mCachedNotifications = new ArrayList<>();
+ mNotificationInfoToCallMap = new HashMap<>();
+ mServices = new HashMap<>();
+ mAccountHandleToCallMap = new HashMap<>();
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+ mNotificationListener = new NotificationListenerService() {
+ @Override
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ synchronized (mLock) {
+ if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
+ NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+ sbn.getUser());
+ boolean sbnMatched = false;
+ for (Call call : mNotificationPendingCalls) {
+ if (info.matchesCall(call)) {
+ Log.i(this, "onNotificationPosted: found a pending "
+ + "callId=[%s] for the call notification w/ "
+ + "id=[%s]",
+ call.getId(), sbn.getId());
+ mNotificationPendingCalls.remove(call);
+ mNotificationInfoToCallMap.put(info, call);
+ sbnMatched = true;
+ break;
+ }
+ }
+ if (!sbnMatched &&
+ !mCachedNotifications.contains(info) /* don't re-add if update */) {
+ Log.i(this, "onNotificationPosted: could not find a"
+ + "call for the call notification w/ id=[%s]",
+ sbn.getId());
+ // notification may post before we started to monitor the call, cache
+ // this notification and try to match it later with new added call.
+ mCachedNotifications.add(info);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ synchronized (mLock) {
+ NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+ sbn.getUser());
+ mCachedNotifications.remove(info);
+ if (mNotificationInfoToCallMap.isEmpty()) {
+ return;
+ }
+ Call call = mNotificationInfoToCallMap.getOrDefault(info, null);
+ if (call != null) {
+ // TODO: fix potential bug for multiple calls of same voip app.
+ mNotificationInfoToCallMap.remove(info, call);
+ stopFGSDelegation(call);
+ }
+ }
+ }
+ };
+
+ }
+
+ public void startMonitor() {
+ try {
+ mNotificationListener.registerAsSystemService(mContext,
+ new ComponentName(this.getClass().getPackageName(),
+ this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
+ } catch (RemoteException e) {
+ Log.e(this, e, "Cannot register notification listener");
+ }
+ }
+
+ public void stopMonitor() {
+ try {
+ mNotificationListener.unregisterAsSystemService();
+ } catch (RemoteException e) {
+ Log.e(this, e, "Cannot unregister notification listener");
+ }
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (!call.isTransactionalCall()) {
+ return;
+ }
+
+ synchronized (mLock) {
+ PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+ Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
+ k -> new HashSet<>());
+ callList.add(call);
+ CompletableFuture.completedFuture(null).thenComposeAsync(
+ (x) -> {
+ startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
+ call.getCallingPackageIdentity().mCallingPackageUid, call);
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot));
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (!call.isTransactionalCall()) {
+ return;
+ }
+
+ synchronized (mLock) {
+ stopMonitorWorks(call);
+ PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+ Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
+ k -> new HashSet<>());
+ callList.remove(call);
+
+ if (callList.isEmpty()) {
+ stopFGSDelegation(call);
+ }
+ }
+ }
+
+ private void startFGSDelegation(int pid, int uid, Call call) {
+ Log.i(this, "startFGSDelegation for call %s", call.getId());
+ if (mActivityManagerInternal != null) {
+ PhoneAccountHandle handle = call.getTargetPhoneAccount();
+ ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
+ uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
+ false /* isSticky */, String.valueOf(handle.hashCode()),
+ 0 /* foregroundServiceType */,
+ ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL);
+ ServiceConnection fgsConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mServices.put(handle, this);
+ startMonitorWorks(call);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mServices.remove(handle);
+ }
+ };
+ try {
+ if (mActivityManagerInternal
+ .startForegroundServiceDelegate(options, fgsConnection)) {
+ Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+ } else {
+ Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
+ }
+ } catch (Exception e) {
+ Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void stopFGSDelegation(Call call) {
+ synchronized (mLock) {
+ Log.i(this, "stopFGSDelegation of call %s", call);
+ PhoneAccountHandle handle = call.getTargetPhoneAccount();
+ Set<Call> calls = mAccountHandleToCallMap.get(handle);
+
+ // Every call for the package that is losing foreground service delegation should be
+ // removed from tracking maps/contains in this class
+ if (calls != null) {
+ for (Call c : calls) {
+ stopMonitorWorks(c); // remove the call from tacking in this class
+ }
+ }
+
+ mAccountHandleToCallMap.remove(handle);
+
+ if (mActivityManagerInternal != null) {
+ ServiceConnection fgsConnection = mServices.get(handle);
+ if (fgsConnection != null) {
+ mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+ Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
+ }
+ }
+ }
+ }
+
+ private void startMonitorWorks(Call call) {
+ startMonitorNotification(call);
+ }
+
+ private void stopMonitorWorks(Call call) {
+ stopMonitorNotification(call);
+ }
+
+ private void startMonitorNotification(Call call) {
+ synchronized (mLock) {
+ boolean sbnMatched = false;
+ for (NotificationInfo info : mCachedNotifications) {
+ if (info.matchesCall(call)) {
+ Log.i(this, "startMonitorNotification: found a cached call "
+ + "notification for call=[%s]", call);
+ mCachedNotifications.remove(info);
+ mNotificationInfoToCallMap.put(info, call);
+ sbnMatched = true;
+ break;
+ }
+ }
+ if (!sbnMatched) {
+ // Only continue to
+ Log.i(this, "startMonitorNotification: could not find a call"
+ + " notification for the call=[%s];", call);
+ mNotificationPendingCalls.add(call);
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mHandler.postDelayed(() -> future.complete(null), 5000L);
+ future.thenComposeAsync(
+ (x) -> {
+ if (mNotificationPendingCalls.contains(call)) {
+ Log.i(this, "Notification for voip-call %s haven't "
+ + "posted in time, stop delegation.", call.getId());
+ stopFGSDelegation(call);
+ mNotificationPendingCalls.remove(call);
+ return null;
+ }
+ return null;
+ }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot));
+ }
+ }
+ }
+
+ private void stopMonitorNotification(Call call) {
+ mNotificationPendingCalls.remove(call);
+ }
+
+ @VisibleForTesting
+ public void setActivityManagerInternal(ActivityManagerInternal ami) {
+ mActivityManagerInternal = ami;
+ }
+
+ private static class NotificationInfo extends Object {
+ private String mPackageName;
+ private UserHandle mUserHandle;
+
+ NotificationInfo(String packageName, UserHandle userHandle) {
+ mPackageName = packageName;
+ mUserHandle = userHandle;
+ }
+
+ boolean matchesCall(Call call) {
+ PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+ return mPackageName != null && mPackageName.equals(
+ accountHandle.getComponentName().getPackageName())
+ && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof NotificationInfo)) {
+ return false;
+ }
+ NotificationInfo that = (NotificationInfo) obj;
+ return Objects.equals(this.mPackageName, that.mPackageName)
+ && Objects.equals(this.mUserHandle, that.mUserHandle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mUserHandle);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{ NotificationInfo: [mPackageName: ")
+ .append(mPackageName)
+ .append("], [mUserHandle=")
+ .append(mUserHandle)
+ .append("] }");
+ return sb.toString();
+ }
+ }
+
+ @VisibleForTesting
+ public void postNotification(StatusBarNotification statusBarNotification) {
+ mNotificationListener.onNotificationPosted(statusBarNotification);
+ }
+
+ @VisibleForTesting
+ public void removeNotification(StatusBarNotification statusBarNotification) {
+ mNotificationListener.onNotificationRemoved(statusBarNotification);
+ }
+
+ @VisibleForTesting
+ public Set<Call> getCallsForHandle(PhoneAccountHandle handle){
+ return mAccountHandleToCallMap.get(handle);
+ }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
new file mode 100644
index 000000000..a952eb136
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -0,0 +1,110 @@
+/*
+ * 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
new file mode 100644
index 000000000..2916fc64b
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
@@ -0,0 +1,77 @@
+/*
+ * 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/AndroidManifest.xml b/testapps/AndroidManifest.xml
index dd8258ae8..645a42b42 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -259,6 +259,15 @@
</intent-filter>
</service>
+ <service android:name="com.android.server.telecom.testapps.OtherSelfManagedConnectionService"
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+ android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService"/>
+ </intent-filter>
+ </service>
+
<receiver android:exported="false"
android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
android:name="com.android.server.telecom.testapps.SelfManagedCallNotificationReceiver"/>
diff --git a/testapps/res/layout/self_managed_sample_main.xml b/testapps/res/layout/self_managed_sample_main.xml
index d26d6292e..98b879a07 100644
--- a/testapps/res/layout/self_managed_sample_main.xml
+++ b/testapps/res/layout/self_managed_sample_main.xml
@@ -55,6 +55,12 @@
android:layout_height="wrap_content"
android:background="@color/test_call_b_color"
android:text="2"/>
+ <RadioButton
+ android:id="@+id/useAcct3Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/test_call_c_color"
+ android:text="3"/>
</RadioGroup>
<TextView
android:id="@+id/hasFocus"
diff --git a/testapps/res/values/colors.xml b/testapps/res/values/colors.xml
index 3939e7835..9447ac8e3 100644
--- a/testapps/res/values/colors.xml
+++ b/testapps/res/values/colors.xml
@@ -17,4 +17,5 @@
<resources>
<color name="test_call_a_color">#f2eebf</color>
<color name="test_call_b_color">#afc5e6</color>
+ <color name="test_call_c_color">#c5afe6</color>
</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java
new file mode 100644
index 000000000..7bb983082
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java
@@ -0,0 +1,20 @@
+/*
+ * 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.testapps;
+
+public class OtherSelfManagedConnectionService extends SelfManagedConnectionService {
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index d4661ff5d..273b06024 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -46,20 +46,27 @@ public class SelfManagedCallList {
public static String SELF_MANAGED_ACCOUNT_1 = "1";
public static String SELF_MANAGED_ACCOUNT_2 = "2";
+ public static String SELF_MANAGED_ACCOUNT_1A = "1A";
public static String SELF_MANAGED_ACCOUNT_3 = "3";
public static String SELF_MANAGED_NAME_1 = "SuperCall";
public static String SELF_MANAGED_NAME_2 = "Mega Call";
- public static String SELF_MANAGED_NAME_3 = "SM Call";
+ public static String SELF_MANAGED_NAME_1A = "SM Call";
+ public static String SELF_MANAGED_NAME_3 = "Sep Process";
public static String CUSTOM_URI_SCHEME = "custom";
private static SelfManagedCallList sInstance;
private static ComponentName COMPONENT_NAME = new ComponentName(
SelfManagedCallList.class.getPackage().getName(),
SelfManagedConnectionService.class.getName());
+ private static ComponentName OTHER_COMPONENT_NAME = new ComponentName(
+ SelfManagedCallList.class.getPackage().getName(),
+ OtherSelfManagedConnectionService.class.getName());
private static Uri SELF_MANAGED_ADDRESS_1 = Uri.fromParts(PhoneAccount.SCHEME_TEL, "555-1212",
"");
private static Uri SELF_MANAGED_ADDRESS_2 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
"me@test.org", "");
+ private static Uri SELF_MANAGED_ADDRESS_3 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
+ "hilda@test.org", "");
private static Map<String, PhoneAccountHandle> mPhoneAccounts = new ArrayMap();
public static SelfManagedCallList getInstance() {
@@ -101,20 +108,29 @@ public class SelfManagedCallList {
SELF_MANAGED_NAME_1, true /* areCallsLogged */);
registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_2, SELF_MANAGED_ADDRESS_2,
SELF_MANAGED_NAME_2, false /* areCallsLogged */);
- registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_3, SELF_MANAGED_ADDRESS_1,
- SELF_MANAGED_NAME_3, true /* areCallsLogged */);
+ registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1A, SELF_MANAGED_ADDRESS_1,
+ SELF_MANAGED_NAME_1A, true /* areCallsLogged */);
+ registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1A, SELF_MANAGED_ADDRESS_1,
+ SELF_MANAGED_NAME_1A, true /* areCallsLogged */);
+ registerPhoneAccount(context, OTHER_COMPONENT_NAME, SELF_MANAGED_ACCOUNT_3,
+ SELF_MANAGED_ADDRESS_3, SELF_MANAGED_NAME_3, false /* areCallsLogged */);
}
public void registerPhoneAccount(Context context, String id, Uri address, String name,
- boolean areCallsLogged) {
- PhoneAccountHandle handle = new PhoneAccountHandle(COMPONENT_NAME, id);
+ boolean areCallsLogged) {
+ registerPhoneAccount(context, COMPONENT_NAME, id, address, name, areCallsLogged);
+ }
+
+ public void registerPhoneAccount(Context context, ComponentName componentName, String id,
+ Uri address, String name, boolean areCallsLogged) {
+ PhoneAccountHandle handle = new PhoneAccountHandle(componentName, id);
mPhoneAccounts.put(id, handle);
Bundle extras = new Bundle();
extras.putBoolean(PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO, true);
if (areCallsLogged) {
extras.putBoolean(PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, true);
}
- if (id.equals(SELF_MANAGED_ACCOUNT_3)) {
+ if (id.equals(SELF_MANAGED_ACCOUNT_1A)) {
extras.putBoolean(PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true);
}
PhoneAccount.Builder builder = PhoneAccount.builder(handle, name)
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
index 75ceb6261..475f255ef 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
@@ -166,8 +166,10 @@ public class SelfManagedCallListAdapter extends BaseAdapter {
SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE);
if (phoneAccountHandle.getId().equals(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1)) {
result.setBackgroundColor(result.getContext().getColor(R.color.test_call_a_color));
- } else {
+ } else if (phoneAccountHandle.getId().equals(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2)) {
result.setBackgroundColor(result.getContext().getColor(R.color.test_call_b_color));
+ } else {
+ result.setBackgroundColor(result.getContext().getColor(R.color.test_call_c_color));
}
CallAudioState audioState = connection.getCallAudioState();
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 44410d2dc..5cdaf3d75 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -43,8 +43,6 @@ import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
-import com.android.server.telecom.testapps.R;
-
import java.util.Objects;
/**
@@ -66,6 +64,7 @@ public class SelfManagedCallingActivity extends Activity {
private Button mDisableCarMode;
private RadioButton mUseAcct1Button;
private RadioButton mUseAcct2Button;
+ private RadioButton mUseAcct3Button;
private CheckBox mHoldableCheckbox;
private CheckBox mVideoCallCheckbox;
private EditText mNumber;
@@ -165,6 +164,7 @@ public class SelfManagedCallingActivity extends Activity {
}));
mUseAcct1Button = findViewById(R.id.useAcct1Button);
mUseAcct2Button = findViewById(R.id.useAcct2Button);
+ mUseAcct3Button = findViewById(R.id.useAcct3Button);
mHasFocus = findViewById(R.id.hasFocus);
mVideoCallCheckbox = findViewById(R.id.videoCall);
mHoldableCheckbox = findViewById(R.id.holdable);
@@ -183,6 +183,8 @@ public class SelfManagedCallingActivity extends Activity {
return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1);
} else if (mUseAcct2Button.isChecked()) {
return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2);
+ } else if (mUseAcct3Button.isChecked()) {
+ return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
}
return null;
}
@@ -214,8 +216,7 @@ public class SelfManagedCallingActivity extends Activity {
private void placeSelfManagedOutgoingCall() {
TelecomManager tm = TelecomManager.from(this);
- PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
- SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
+ PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
if (mCheckIfPermittedBeforeCalling.isChecked()) {
Toast.makeText(this, R.string.outgoingCallNotPermitted, Toast.LENGTH_SHORT).show();
@@ -264,7 +265,7 @@ public class SelfManagedCallingActivity extends Activity {
private void placeSelfManagedIncomingCall() {
TelecomManager tm = TelecomManager.from(this);
PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
- SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
+ SelfManagedCallList.SELF_MANAGED_ACCOUNT_1A);
if (mCheckIfPermittedBeforeCalling.isChecked()) {
if (!tm.isIncomingCallPermitted(phoneAccountHandle)) {
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
index e6e35d87d..3ef8fbb75 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
@@ -87,6 +87,7 @@ public class SelfManagedConnectionService extends ConnectionService {
mCallList.notifyConnectionServiceFocusGained();
}
+ @SuppressWarnings("CatchAndPrintStackTrace")
private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming,
boolean isHandover) {
SelfManagedConnection connection = new SelfManagedConnection(mCallList,
@@ -101,11 +102,13 @@ public class SelfManagedConnectionService extends ConnectionService {
connection.setCallerDisplayName(TEST_NAMES[random.nextInt(TEST_NAMES.length)],
TelecomManager.PRESENTATION_ALLOWED);
connection.setExtras(request.getExtras());
- if (isIncoming) {
- connection.setIsIncomingCallUiShowing(request.shouldShowIncomingCallUi());
- connection.setRinging();
- } else {
- connection.setDialing();
+ if (!request.getAddress().getSchemeSpecificPart().equals("123")) {
+ if (isIncoming) {
+ connection.setIsIncomingCallUiShowing(request.shouldShowIncomingCallUi());
+ connection.setRinging();
+ } else {
+ connection.setDialing();
+ }
}
Bundle requestExtras = request.getExtras();
if (requestExtras != null) {
diff --git a/testapps/streamingtest/Android.bp b/testapps/streamingtest/Android.bp
new file mode 100644
index 000000000..bd0a582ea
--- /dev/null
+++ b/testapps/streamingtest/Android.bp
@@ -0,0 +1,31 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "streamingTestApp",
+ static_libs: [
+ "androidx.legacy_legacy-support-v4",
+ "guava",
+ ],
+ srcs: ["src/**/*.java"],
+ platform_apis: true,
+ certificate: "platform",
+ privileged: true,
+}
diff --git a/testapps/streamingtest/AndroidManifest.xml b/testapps/streamingtest/AndroidManifest.xml
new file mode 100644
index 000000000..47e4abc43
--- /dev/null
+++ b/testapps/streamingtest/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.server.telecom.streamingtest">
+
+ <uses-sdk android:minSdkVersion="28"
+ android:targetSdkVersion="33"/>
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION"/>
+
+ <application android:label="Streaming Test App">
+ <uses-library android:name="android.test.runner"/>
+
+ <service android:name="com.android.server.telecom.streamingtest.StreamingService"
+ android:exported="true"
+ android:permission="android.permission.BIND_CALL_STREAMING_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecom.CallStreamingService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
new file mode 100644
index 000000000..c76b3495c
--- /dev/null
+++ b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
@@ -0,0 +1,46 @@
+/*
+ * 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.streamingtest;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.telecom.CallStreamingService;
+import android.telecom.StreamingCall;
+import android.telecom.Log;
+
+public class StreamingService extends CallStreamingService {
+ @Override
+ public void onCallStreamingStarted(@NonNull StreamingCall call) {
+ Log.i(this, "onCallStreamingStarted: call %s", call);
+ }
+
+ @Override
+ public void onCallStreamingStopped() {
+ Log.i(this, "onCallStreamingStopped");
+ }
+
+ @Override
+ public void onCallStreamingStateChanged(int state) {
+ Log.i(this, "onCallStreamingStateChanged; state=%d", state);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ Log.i(this, "onUnbind");
+ return false;
+ }
+}
diff --git a/testapps/transactionalVoipApp/Android.bp b/testapps/transactionalVoipApp/Android.bp
new file mode 100644
index 000000000..68089e2ae
--- /dev/null
+++ b/testapps/transactionalVoipApp/Android.bp
@@ -0,0 +1,28 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "transactionalVoipApp",
+ static_libs: [
+ "androidx.legacy_legacy-support-v4",
+ "guava",
+ ],
+ srcs: ["src/**/*.java"],
+}
diff --git a/testapps/transactionalVoipApp/AndroidManifest.xml b/testapps/transactionalVoipApp/AndroidManifest.xml
new file mode 100644
index 000000000..e4968dbe2
--- /dev/null
+++ b/testapps/transactionalVoipApp/AndroidManifest.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.server.telecom.transactionalVoipApp">
+
+ <uses-sdk android:minSdkVersion="28"
+ android:targetSdkVersion="33"/>
+
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
+ <!-- Needed to test media/audio -->
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <!-- Needed for foreground services -->
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
+
+ <application android:label="Transactional Voip">
+ <uses-library android:name="android.test.runner"/>
+
+ <activity android:name="com.android.server.telecom.transactionalVoipApp.VoipAppMainActivity"
+ android:exported="true"
+ android:label="Transactional Voip">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity android:name="com.android.server.telecom.transactionalVoipApp.InCallActivity"
+ android:exported="true"
+ android:launchMode="singleInstance"
+ android:label="InCall VoIP Activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <service
+ android:name=".BackgroundIncomingCallService"
+ android:foregroundServiceType="phoneCall"
+ android:exported="false"
+ />
+
+ </application>
+</manifest>
diff --git a/testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.png
new file mode 100644
index 000000000..ed3ee454d
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.png
new file mode 100644
index 000000000..a4add5149
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.png
new file mode 100644
index 000000000..41558f251
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.png
new file mode 100644
index 000000000..6006b1207
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.png
new file mode 100644
index 000000000..4f935bf59
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/layout/in_call_activity.xml b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
new file mode 100644
index 000000000..a92a99bb9
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/getCallIdTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/get_call_id"
+ />
+
+ <Button
+ android:id="@+id/updateCallStyleNotification"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/update_notification"
+ />
+
+ <Button
+ android:id="@+id/answer_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/answer"/>
+
+ <Button
+ android:id="@+id/set_call_active_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/set_call_active"/>
+
+ <Button
+ android:id="@+id/set_call_inactive_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/set_call_inactive"/>
+
+ <Button
+ android:id="@+id/disconnect_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnect_call"/>
+
+ <Button
+ android:id="@+id/start_stream_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_stream"/>
+
+ <Button
+ android:id="@+id/crash_app"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/crash_app"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/current_endpoint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/request_earpiece"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/request_earpiece_endpoint"/>
+
+ <Button
+ android:id="@+id/request_speaker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/request_speaker_endpoint"/>
+
+ <Button
+ android:id="@+id/request_bluetooth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/request_bluetooth_endpoint"/>
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/testapps/transactionalVoipApp/res/layout/main_activity.xml b/testapps/transactionalVoipApp/res/layout/main_activity.xml
new file mode 100644
index 000000000..28f074457
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/layout/main_activity.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/registerButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/register_phone_account"/>
+
+ <Button
+ android:id="@+id/startForegroundService"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_foreground_service"
+ />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/startOutgoingCall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_outgoing"
+ />
+
+ <Button
+ android:id="@+id/startIncomingCall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_incoming"
+ />
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/testapps/transactionalVoipApp/res/raw/sample_audio.ogg b/testapps/transactionalVoipApp/res/raw/sample_audio.ogg
new file mode 100644
index 000000000..0129b46fd
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/raw/sample_audio.ogg
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/raw/sample_audio2.ogg b/testapps/transactionalVoipApp/res/raw/sample_audio2.ogg
new file mode 100644
index 000000000..a0b39b4ec
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/raw/sample_audio2.ogg
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/values-af/strings.xml b/testapps/transactionalVoipApp/res/values-af/strings.xml
new file mode 100644
index 000000000..bf7ad3353
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-af/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API-toetsaktiwiteit"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaksionele inoproepaktiwiteit"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registreer foonrekening"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Begin voorgronddiens (simuleer masjienvertaling + app op agtergrond)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Begin uitgaande oproep"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Begin inkomende oproep"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"oproep-id is nie gestel nie"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"antwoord"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ontkoppel"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Oorstuk"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Luidspreker"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"begin stroom"</string>
+ <string name="crash_app" msgid="2548690390730057704">"gooi uitsondering"</string>
+ <string name="update_notification" msgid="8677916482672588779">"dateer kennisgewing aan voortdurende oproepstyl op"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-am/strings.xml b/testapps/transactionalVoipApp/res/values-am/strings.xml
new file mode 100644
index 000000000..d71c28739
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-am/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"የግብይት ኤፒአይ ሙከራ እንቅስቃሴ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"በጥሪ እንቅስቃሴ ውስጥ ግብይታዊ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"የስልክ መለያ መዝግብ"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS ይጀምሩ (በዳራው ውስጥ MT + መተግበሪያን ያስመስላል)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ወጪ ጥሪን ይጀምሩ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ገቢ ጥሪን ይጀምሩ"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"የደዋይ መታወቂያ አልተቀናበረም"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"ወደ ገቢር ተቀናብሯል"</string>
+ <string name="answer" msgid="5423590397665409939">"መልስ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"ወደ ገቢር ያልሆነ ተቀናብሯል"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ግንኙነትን ያቋርጡ"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ማዳመጫ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ድምፅ ማውጫ"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ብሉቱዝ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ዥረት ይጀምሩ"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ለየት ያለ ነገርን ይጣሉ"</string>
+ <string name="update_notification" msgid="8677916482672588779">"በመካሄድ ላይ ላለ ጥሪ ቅጥ ማሳወቂያ ያዘምኑ"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ar/strings.xml b/testapps/transactionalVoipApp/res/values-ar/strings.xml
new file mode 100644
index 000000000..d2c146491
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ar/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"نشاط اختبار واجهة برمجة التطبيقات من خلال المعاملات"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"نشاط المعاملات أثناء المكالمة"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"تسجيل حساب الهاتف"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"‏بدء FGS (محاكاة الترجمة الآلية + التطبيق في الخلفية)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"بدء مكالمة صادرة"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"بدء مكالمة واردة"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"لم يتم ضبط رقم تعريف المكالمة"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"الإجابة"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"إلغاء الربط"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"سماعة الأذن"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"مكبّر الصوت"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"البلوتوث"</string>
+ <string name="start_stream" msgid="3567634786280097431">"بدء البث"</string>
+ <string name="crash_app" msgid="2548690390730057704">"طرح استثناء"</string>
+ <string name="update_notification" msgid="8677916482672588779">"إشعار التعديل إلى نمط المكالمات الجارية"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-as/strings.xml b/testapps/transactionalVoipApp/res/values-as/strings.xml
new file mode 100644
index 000000000..c48ac0e6c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-as/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"লেনদেন সম্বন্ধীয় API পৰীক্ষণৰ কাৰ্যকলাপ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"কলত হোৱা লেনদেন সম্বন্ধীয় কাৰ্যকলাপ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ফ\'নৰ একাউণ্ট পঞ্জীয়ন কৰক"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS আৰম্ভ কৰক (নেপথ্যত MT + এপ্ ছিমুলে’ট কৰক)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"বহিৰ্গামী কল আৰম্ভ কৰক"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"অন্তৰ্গামী কল আৰম্ভ কৰক"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"কলৰ আইডিটো ছেট কৰা হোৱা নাই"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"সক্ৰিয় হিচাপে ছেট কৰক"</string>
+ <string name="answer" msgid="5423590397665409939">"উত্তৰ দিয়ক"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"নিষ্ক্ৰিয় হিচাপে ছেট কৰক"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"সংযোগ বিচ্ছিন্ন কৰক"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ইয়েৰপিচ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"স্পীকাৰ"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ব্লুটুথ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ষ্ট্ৰীম কৰিবলৈ আৰম্ভ কৰক"</string>
+ <string name="crash_app" msgid="2548690390730057704">"থ্ৰ’ এক্সচেপশ্বন"</string>
+ <string name="update_notification" msgid="8677916482672588779">"চলিত কলৰ শৈলী সম্পৰ্কে আপডে’ট দিয়া জাননী"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-az/strings.xml b/testapps/transactionalVoipApp/res/values-az/strings.xml
new file mode 100644
index 000000000..75d82784d
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-az/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Tranzaksiya ilə bağlı API test Fəaliyyəti"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Tranzaksiya üzrə Zəngdaxili Fəaliyyət"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefon Hesabını Qeydiyyatdan Keçirin"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS-ni başladın (arxa fonda MT + tətbiqini simulyasiya edin)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Gedən zəng başladın"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Gələn zəng başladın"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"zəng ID-si təyin olunmayıb"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Aktiv kimi təyin edin"</string>
+ <string name="answer" msgid="5423590397665409939">"cavab"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Qeyri-aktiv kimi təyin edin"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"əlaqəni kəsin"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Qulaqlıq"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Dinamik"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"yayıma başlayın"</string>
+ <string name="crash_app" msgid="2548690390730057704">"istisna yaradın"</string>
+ <string name="update_notification" msgid="8677916482672588779">"bildirişi davam edən zəng üslubuna yeniləyin"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-b+sr+Latn/strings.xml b/testapps/transactionalVoipApp/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 000000000..f82491064
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktivnost testiranja transakcionog API-ja"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Aktivnost poziva u vezi sa transakcijama"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registruj nalog telefona"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Pokreni FGS (simulirajte MT + aplikaciju u pozadini)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Započnite odlazni poziv"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Započnite dolazni poziv"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID poziva nije podešen"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"odgovori"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"prekini vezu"</string>
+ <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">"počnite da strimujete"</string>
+ <string name="crash_app" msgid="2548690390730057704">"izbaciti izuzetak"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ažurirajte obaveštenje na stil aktuelnog poziva"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-be/strings.xml b/testapps/transactionalVoipApp/res/values-be/strings.xml
new file mode 100644
index 000000000..36d558ecf
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-be/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Праверачныя дзеянні API трансакцый"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Дзеянні падчас выклікаў"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Зарэгістраваць уліковы запіс тэлефона"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Запусціць FGS (сімуляцыя MT + праграма ў фонавым рэжыме)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Пачаць выходны выклік"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Пачаць уваходны выклік"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ідэнтыфікатар выкліку не зададзены"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"адказаць"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"завяршыць выклік"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Навушнік"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Дынамік"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"пачаць перадачу плынню"</string>
+ <string name="crash_app" msgid="2548690390730057704">"адправіць паведамленне аб выключэнні"</string>
+ <string name="update_notification" msgid="8677916482672588779">"стыль паведамлення аб абнаўленні для бягучага званка"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-bg/strings.xml b/testapps/transactionalVoipApp/res/values-bg/strings.xml
new file mode 100644
index 000000000..2210400a8
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-bg/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Активност за тестване на API за транзакции"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Транзакционална активност в обаждане"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Регистриране на профила на телефона"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Стартиране на FGS (симулиране на MT + приложението на заден план)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Започване на изходящо обаждане"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Започване на входящо обаждане"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"идентификаторът на обаждането не е зададен"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"отговаряне"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"прекратяване на връзката"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Слушалка"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Високоговорител"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"започване на поточно предаване"</string>
+ <string name="crash_app" msgid="2548690390730057704">"генериране на изключение"</string>
+ <string name="update_notification" msgid="8677916482672588779">"актуализиране на известието до стила на текущото обаждане"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-bn/strings.xml b/testapps/transactionalVoipApp/res/values-bn/strings.xml
new file mode 100644
index 000000000..45f13be68
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-bn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API টেস্ট সংক্রান্ত অ্যাক্টিভিটি"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"কল অ্যাক্টিভিটিতে হওয়া ট্রানজ্যাকশন"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ফোনের অ্যাকাউন্ট রেজিস্টার করুন"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS শুরু করুন (সিমুলেট MT + ব্যাকগ্রাউন্ডে থাকা অ্যাপ)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"আউটগোয়িং কল শুরু করুন"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ইনকামিং কল শুরু করুন"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"কলার আইডি সেট করা নেই"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"উত্তর দিন"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ডিসকানেক্ট করুন"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ইয়ারপিস"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"স্পিকার"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ব্লুটুথ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"স্ট্রিমিং শুরু করুন"</string>
+ <string name="crash_app" msgid="2548690390730057704">"এক্সেপশন যোগ করুন"</string>
+ <string name="update_notification" msgid="8677916482672588779">"চালু থাকা কলের স্টাইলে আপডেট সংক্রান্ত বিজ্ঞপ্তি"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-bs/strings.xml b/testapps/transactionalVoipApp/res/values-bs/strings.xml
new file mode 100644
index 000000000..24ffba2d3
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-bs/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktivnost testa transakcijskog API-ja"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transakcijska aktivnost u pozivu"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrirajte račun telefona"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Pokreni FGS (simuliraj MT i aplikaciju u pozadini)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Pokreni odlazni poziv"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Pokreni dolazni poziv"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID poziva nije postavljen"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"postavi na Aktivno"</string>
+ <string name="answer" msgid="5423590397665409939">"odgovori"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"postavi na Neaktivno"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"prekini vezu"</string>
+ <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="crash_app" msgid="2548690390730057704">"izbaci izuzetak"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ažuriraj obavještenje u stil poziva u toku"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml
new file mode 100644
index 000000000..06f165569
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ca/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Activitat de prova de l\'API transaccional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Activitat de transaccions durant la trucada"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registra el compte del telèfon"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Inicia FGS (simula MT + aplicació en segon pla)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Inicia una trucada sortint"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Inicia una trucada entrant"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"identificador de trucada no definit"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"defineix com a activa"</string>
+ <string name="answer" msgid="5423590397665409939">"respon"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"defineix com a inactiva"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"desconnecta"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altaveu"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"inicia la reproducció en continu"</string>
+ <string name="crash_app" msgid="2548690390730057704">"llança una excepció"</string>
+ <string name="update_notification" msgid="8677916482672588779">"actualitza la notificació a l\'estil de trucada en curs"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-cs/strings.xml b/testapps/transactionalVoipApp/res/values-cs/strings.xml
new file mode 100644
index 000000000..66327650f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-cs/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktivita testování v transakčním rozhraní API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transakční aktivita během hovoru"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrovat telefonní účet"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Spustit službu v popředí (simulovat MT a aplikaci v pozadí)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Zahájit odchozí hovor"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Zahájit příchozí hovor"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID hovoru není nastaveno"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"odpověď"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"odpojit"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Sluchátko"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Reproduktor"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"zahájit streamování"</string>
+ <string name="crash_app" msgid="2548690390730057704">"vyvolat výjimku"</string>
+ <string name="update_notification" msgid="8677916482672588779">"styl aktualizace oznámení o probíhajícím hovoru"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-da/strings.xml b/testapps/transactionalVoipApp/res/values-da/strings.xml
new file mode 100644
index 000000000..1a23b5838
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-da/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Testaktivitet for transaktions-API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaktionsrelateret aktivitet i opkald"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrer telefonkonto"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Start FGS (simuler maskinoversættelse + app i baggrunden)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Start udgående opkald"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Start indgående opkald"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"opkalds-id ikke konfigureret"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Indstil som aktiv"</string>
+ <string name="answer" msgid="5423590397665409939">"svar"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Indstil som inaktiv"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"afslut opkald"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Højttaler"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Højttaler"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"start med at streame"</string>
+ <string name="crash_app" msgid="2548690390730057704">"udløs en undtagelse"</string>
+ <string name="update_notification" msgid="8677916482672588779">"opdateringsnotifikation til igangværende opkaldsstil"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-de/strings.xml b/testapps/transactionalVoipApp/res/values-de/strings.xml
new file mode 100644
index 000000000..4f853fcac
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-de/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Testaktivität zur transaktionalen API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaktionsaktivität bei aktiven Anruf"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefonkonto registrieren"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS starten (MT und App im Hintergrund simulieren)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Ausgehenden Anruf starten"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Eingehenden Anruf starten"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"Anrufer-ID nicht festgelegt"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"aktiv"</string>
+ <string name="answer" msgid="5423590397665409939">"annehmen"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"inaktiv"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"beenden"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Kopfhörer"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Lautsprecher"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Streaming starten"</string>
+ <string name="crash_app" msgid="2548690390730057704">"Ausnahme auslösen"</string>
+ <string name="update_notification" msgid="8677916482672588779">"Benachrichtigung zum Stil des laufenden Anrufs aktualisieren"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-el/strings.xml b/testapps/transactionalVoipApp/res/values-el/strings.xml
new file mode 100644
index 000000000..555398199
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-el/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Δοκιμαστική δραστηριότητα API συναλλαγών"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Δραστηριότητα συναλλαγής στην κλήση"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Εγγραφή λογαριασμού τηλεφώνου"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Έναρξη FGS (προσομοίωση MT + εφαρμογή στο παρασκήνιο)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Έναρξη εξερχόμενης κλήσης"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Έναρξη εισερχόμενης κλήσης"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"δεν έχει οριστεί αναγνωριστικό κλήσης"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"απάντηση"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"αποσύνδεση"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Ακουστικό τηλεφώνου"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Ηχείο"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"έναρξη ροής"</string>
+ <string name="crash_app" msgid="2548690390730057704">"εμφάνιση εξαίρεσης"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ενημέρωση ειδοποίησης στο στιλ κλήσης σε εξέλιξη"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-en-rAU/strings.xml b/testapps/transactionalVoipApp/res/values-en-rAU/strings.xml
new file mode 100644
index 000000000..bf68cf535
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rAU/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API test activity"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transactional in-call activity"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Register phone account"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Start FGS (simulate MT + app in background)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Start outgoing call"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Start incoming call"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"call ID not set"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"answer"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"disconnect"</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>
+ <string name="start_stream" msgid="3567634786280097431">"start streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"throw exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"Update notification to ongoing call style"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-en-rCA/strings.xml b/testapps/transactionalVoipApp/res/values-en-rCA/strings.xml
new file mode 100644
index 000000000..269f0d3ff
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rCA/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API test Activity"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transactional In Call Activity"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Register Phone Account"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Start FGS (simulate MT + app in background)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Start Outgoing Call"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Start Incoming Call"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"call id not set"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"answer"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"disconnect"</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>
+ <string name="start_stream" msgid="3567634786280097431">"start streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"throw exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"update notification to ongoing call style"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-en-rGB/strings.xml b/testapps/transactionalVoipApp/res/values-en-rGB/strings.xml
new file mode 100644
index 000000000..bf68cf535
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rGB/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API test activity"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transactional in-call activity"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Register phone account"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Start FGS (simulate MT + app in background)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Start outgoing call"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Start incoming call"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"call ID not set"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"answer"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"disconnect"</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>
+ <string name="start_stream" msgid="3567634786280097431">"start streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"throw exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"Update notification to ongoing call style"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-en-rIN/strings.xml b/testapps/transactionalVoipApp/res/values-en-rIN/strings.xml
new file mode 100644
index 000000000..bf68cf535
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rIN/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API test activity"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transactional in-call activity"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Register phone account"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Start FGS (simulate MT + app in background)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Start outgoing call"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Start incoming call"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"call ID not set"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"answer"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"disconnect"</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>
+ <string name="start_stream" msgid="3567634786280097431">"start streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"throw exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"Update notification to ongoing call style"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-en-rXC/strings.xml b/testapps/transactionalVoipApp/res/values-en-rXC/strings.xml
new file mode 100644
index 000000000..d94683ace
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rXC/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‎‏‎‎‎‎‏‎‏‏‎‏‎‏‎‎‏‏‎‎‎‏‎‏‏‏‎‎‎‎‎‏‏‏‎‎‎‏‎‎‎‏‏‎‏‏‏‏‎‏‎‎‎‏‎‎‎‎‎‏‏‎Transactional API test Activity‎‏‎‎‏‎"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‎‎‎‎‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‎‎‏‎‏‎‏‎‏‏‎‎‎‏‎‏‏‎‎‎‏‎Transactional In Call Activity‎‏‎‎‏‎"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎‏‏‎‎‏‎‏‎‏‎‏‎‎‏‏‎‎‎‏‏‎‏‏‎‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‎‏‏‏‏‏‏‎‎‎Register Phone Account‎‏‎‎‏‎"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‏‎‏‎‏‏‏‏‎‎‏‎‏‎‎‏‏‎‎‎‎‏‏‎‎‎‎‎‏‎‎‏‎‏‎‎‎‏‏‏‏‏‏‏‏‎‎Start FGS (simulate MT + app in background)‎‏‎‎‏‎"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‎‏‎‎‏‎‎‎‎‎‎‎‏‎‏‎‎‏‏‏‎‎‎‎‎‎‎‏‎‎‎‎Start Outgoing Call‎‏‎‎‏‎"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‎‎‎‏‎‎‏‎‏‏‏‎‎‎‏‎‏‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‏‎‏‏‎‎‏‏‎‏‏‎‏‏‎‏‏‏‎Start Incoming Call‎‏‎‎‏‎"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‎‎‏‏‏‎‎‎‏‎‏‎‏‏‎‏‎‏‏‎‎‎‏‏‎‎‏‎‎‎call id not set‎‏‎‎‏‎"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‎‏‏‏‎‏‎‏‏‎‏‎‎‎‏‎‏‎‎‎‏‎‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‎‎‎‏‏‏‏‎‎‎‎‏‏‏‏‎‎‎‎‏‎‏‏‎setActive‎‏‎‎‏‎"</string>
+ <string name="answer" msgid="5423590397665409939">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‎‎‎‏‎‎‎‏‏‏‎‏‏‎‏‎‏‏‏‎‏‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‏‎‎‏‏‎answer‎‏‎‎‏‎"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‏‎‎‎‎‎‎‏‎‏‎‏‏‎‎‏‏‎‎‏‏‎‏‏‎‎‎‏‎‎‎‎‏‎‏‏‏‎‏‏‏‎‏‏‎‎‏‎‏‎‏‎‏‏‎setInactive‎‏‎‎‏‎"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‎‎‏‎‏‎‏‏‏‎‏‎‎‎‎‏‎‎‏‏‎‏‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‎‎‏‎disconnect‎‏‎‎‏‎"</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>
+ <string name="start_stream" msgid="3567634786280097431">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‏‎‎‎‎‎‏‎‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‏‎‏‎‎‏‎‏‏‏‎start streaming‎‏‎‎‏‎"</string>
+ <string name="crash_app" msgid="2548690390730057704">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‏‏‎‏‏‎‎‎‏‎‎‎‏‏‎‏‎‎‎‎‎‏‎‎‏‎‏‏‏‏‎‏‎‏‎‏‎‏‎‎‏‏‏‏‏‏‎‏‎‎‎‎throw exception‎‏‎‎‏‎"</string>
+ <string name="update_notification" msgid="8677916482672588779">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‏‎‎‎‏‎‎‏‏‏‎‎‎‏‎‎‎‎‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‎‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‏‎update notification to ongoing call style‎‏‎‎‏‎"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-es-rUS/strings.xml b/testapps/transactionalVoipApp/res/values-es-rUS/strings.xml
new file mode 100644
index 000000000..da554d148
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-es-rUS/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Actividad de prueba de la API transaccional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Actividad transaccional en las llamadas"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrar cuenta telefónica"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Iniciar FGS (simulación de TA y app en segundo plano)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Iniciar llamada saliente"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Iniciar llamada entrante"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"no se estableció el identificador de llamadas"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"responder"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"desconectar"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Bocina"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Iniciar transmisión"</string>
+ <string name="crash_app" msgid="2548690390730057704">"generación de excepción"</string>
+ <string name="update_notification" msgid="8677916482672588779">"notificación de actualización del estilo de llamada en curso"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-es/strings.xml b/testapps/transactionalVoipApp/res/values-es/strings.xml
new file mode 100644
index 000000000..b3f2919b9
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-es/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Actividad de prueba de API transaccional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Actividad transaccional durante la llamada"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrar cuenta de teléfono"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Iniciar FGS (simular MT + aplicación en segundo plano)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Iniciar llamada saliente"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Iniciar llamada entrante"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"identificador de llamada no definido"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Activar"</string>
+ <string name="answer" msgid="5423590397665409939">"responder"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Desactivar"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"desconectar"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altavoz"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"iniciar emisión"</string>
+ <string name="crash_app" msgid="2548690390730057704">"excepción de expresión \"throw\""</string>
+ <string name="update_notification" msgid="8677916482672588779">"actualizar notificación al estilo de llamada en curso"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-et/strings.xml b/testapps/transactionalVoipApp/res/values-et/strings.xml
new file mode 100644
index 000000000..4cc5aabd6
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-et/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Tehingupõhise API testimise tegevus"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Kõnesisene toimingutegevus"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefonikonto registreerimine"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Käivita FGS (simuleeri taustal MT-d ja rakendust)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Alusta väljuvat kõnet"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Alusta sissetulevat kõnet"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"helistaja ID pole seadistatud"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"vastus"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"katkesta ühendus"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Kuular"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Kõlar"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"käivita voogesitus"</string>
+ <string name="crash_app" msgid="2548690390730057704">"erandi viskamine"</string>
+ <string name="update_notification" msgid="8677916482672588779">"värskendage märguannet käimasoleva kõne stiilis"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-eu/strings.xml b/testapps/transactionalVoipApp/res/values-eu/strings.xml
new file mode 100644
index 000000000..8b3a1814b
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-eu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transakzio bidezko APIen proba-jarduerak"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Deiko transakzio-jarduerak"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Erregistratu telefonoaren kontua"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Hasi FGS (simulatu itzulpen automatikoa + aplikazioa atzeko planoan)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Hasi irteerako dei bat simulatzen"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Hasi sarrerako dei bat simulatzen"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ez da ezarri deiaren identifikatzailea"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"erantzun"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"deskonektatu"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Aurikularrak"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Bozgorailua"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetootha"</string>
+ <string name="start_stream" msgid="3567634786280097431">"hasi zuzenean igortzen"</string>
+ <string name="crash_app" msgid="2548690390730057704">"eman salbuespena"</string>
+ <string name="update_notification" msgid="8677916482672588779">"eguneratu jakinarazpena, abian den deiaren estiloarekin bat etor dadin"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fa/strings.xml b/testapps/transactionalVoipApp/res/values-fa/strings.xml
new file mode 100644
index 000000000..88143cb5f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fa/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"‏فعالیت آزمایشی Transactional API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"تبادلی در فعالیت تماس"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ثبت حساب تلفن"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"‏شروع FGS (شبیه‌سازی ترجمه ماشینی + برنامه در پس‌زمینه)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"شروع تماس خروجی"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"شروع تماس ورودی"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"شناسه تماس تنظیم نشده است"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"تنظیم به‌عنوان فعال"</string>
+ <string name="answer" msgid="5423590397665409939">"پاسخ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"تنظیم به‌عنوان غیرفعال"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"قطع ارتباط"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"گوشی"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"بلندگو"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"بلوتوث"</string>
+ <string name="start_stream" msgid="3567634786280097431">"شروع جاری‌سازی"</string>
+ <string name="crash_app" msgid="2548690390730057704">"استثنا قائل شدن"</string>
+ <string name="update_notification" msgid="8677916482672588779">"به‌روزرسانی اعلان به‌سبک تماس درحال انجام"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fi/strings.xml b/testapps/transactionalVoipApp/res/values-fi/strings.xml
new file mode 100644
index 000000000..673d56dba
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fi/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Tapahtuman API-testitoiminta"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Tapahtuman puhelunaikainen toiminta"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Rekisteröi puhelintili"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Käynnistä FGS (simuloi MT + sovellus taustalla)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Aloita lähtevä puhelu"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Aloita saapuva puhelu"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"soittajan tunnusta ei asetettu"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"aseta aktiiviseksi"</string>
+ <string name="answer" msgid="5423590397665409939">"vastaa"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"aseta ei-aktiiviseksi"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"katkaise yhteys"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Kaiutin"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Kaiutin"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"aloita suoratoisto"</string>
+ <string name="crash_app" msgid="2548690390730057704">"lähetyspoikkeus"</string>
+ <string name="update_notification" msgid="8677916482672588779">"päivitä ilmoitus käynnissä olevan puhelun tyyliin"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
new file mode 100644
index 000000000..d58aa1395
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Activité de test de l\'API transactionnelle"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Activité transactionnelle durant l\'appel"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Inscrire un compte téléphonique"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + application en arrière-plan)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Démarrer un appel sortant"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Démarrer un appel entrant"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"identifiant de l\'appel non défini"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"répondre"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"déconnecter"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Écouteur"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Haut-parleur"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"démarrer une diffusion"</string>
+ <string name="crash_app" msgid="2548690390730057704">"générer une exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"modifier la notification en fonction du style de l\'appel en cours"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fr/strings.xml b/testapps/transactionalVoipApp/res/values-fr/strings.xml
new file mode 100644
index 000000000..780b8e8c5
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Activité de test de l\'API transactionnelle"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Activité transactionnelle en cours d\'appel"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Enregistrer un compte de téléphonie"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer les services de premier plan (simuler la MT + l\'application en arrière-plan)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Démarrer un appel sortant"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Démarrer un appel entrant"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"affichage du numéro de l\'appelant non défini"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Définir comme actif"</string>
+ <string name="answer" msgid="5423590397665409939">"réponse"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Définir comme inactif"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"raccrocher"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Écouteur"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Haut-parleur"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"démarrer la diffusion"</string>
+ <string name="crash_app" msgid="2548690390730057704">"générer une exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"modifier la notification en fonction du style de l\'appel en cours"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-gl/strings.xml b/testapps/transactionalVoipApp/res/values-gl/strings.xml
new file mode 100644
index 000000000..f168ab26a
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-gl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Actividade de proba da API transaccional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Actividade transaccional nas chamadas"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Rexistrar conta do teléfono"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Iniciar FGS (simular MT + aplicación en segundo plano)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Iniciar chamada saínte"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Iniciar chamada entrante"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"identificador de chamada non definido"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"responder"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"desconectar"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altofalante"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"iniciar reprodución en tempo real"</string>
+ <string name="crash_app" msgid="2548690390730057704">"activar excepción"</string>
+ <string name="update_notification" msgid="8677916482672588779">"actualiza a notificación en función do estilo da chamada en curso"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-gu/strings.xml b/testapps/transactionalVoipApp/res/values-gu/strings.xml
new file mode 100644
index 000000000..60bb0b76f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-gu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional APIના પરીક્ષણની પ્રવૃત્તિ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"કૉલમાંની વ્યવહારિક પ્રવૃત્તિ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ફોન એકાઉન્ટ રજિસ્ટર કરો"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS (MT સિમ્યુલેટ કરવું + બૅકગ્રાઉન્ડમાં ઍપ) શરૂ કરો"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"આઉટગોઇંગ કૉલ શરૂ કરો"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ઇનકમિંગ કૉલ શરૂ કરો"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"કૉલર ID સેટ કરેલું નથી"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"સક્રિય તરીકે સેટ કરો"</string>
+ <string name="answer" msgid="5423590397665409939">"જવાબ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"નિષ્ક્રિય તરીકે સેટ કરો"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ડિસ્કનેક્ટ કરો"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ઇયરપીસ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"સ્પીકર"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"બ્લૂટૂથ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"સ્ટ્રીમિંગ શરૂ કરો"</string>
+ <string name="crash_app" msgid="2548690390730057704">"અપવાદ થ્રો કરો"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ચાલુ કૉલ શૈલી પર નોટિફિકેશન અપડેટ કરો"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hi/strings.xml b/testapps/transactionalVoipApp/res/values-hi/strings.xml
new file mode 100644
index 000000000..ba4262aa0
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hi/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API से जुड़ी टेस्ट गतिविधि"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"कॉल में क्लाइंट और सर्वर के बीच हुई बातचीत से जुड़ी गतिविधि"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Phone Account में रजिस्टर करें"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS शुरू करें (बैकग्राउंड में MT + ऐप्लिकेशन को सिम्युलेट करें)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"आउटगोइंग कॉल शुरू करें"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"इनकमिंग कॉल शुरू करें"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"कॉल आईडी सेट नहीं है"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"जवाब"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"डिसकनेक्ट करें"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ईयरपीस"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"स्पीकर"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ब्लूटूथ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"स्ट्रीमिंग शुरू करें"</string>
+ <string name="crash_app" msgid="2548690390730057704">"अपवाद जोड़ें"</string>
+ <string name="update_notification" msgid="8677916482672588779">"मौजूदा कॉल की स्टाइल के हिसाब से सूचनाओं को अपडेट करें"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hr/strings.xml b/testapps/transactionalVoipApp/res/values-hr/strings.xml
new file mode 100644
index 000000000..c324f6db7
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Testna aktivnost API-ja za transakcije"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"transakcijska aktivnost u pozivu"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registracija telefonskog računa"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Pokretanje FGS-a (simulacija: MT i aplikacija u pozadini)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Pokretanje odlaznog poziva"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Pokretanje dolaznog poziva"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"id poziva nije postavljen"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Postavljanje kao aktivno"</string>
+ <string name="answer" msgid="5423590397665409939">"odgovor"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Postavljanje kao neaktivno"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"prekid veze"</string>
+ <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">"pokretanje streaminga"</string>
+ <string name="crash_app" msgid="2548690390730057704">"izbacivanje iznimke"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ažuriranje obavijesti u stil poziva u tijeku"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hu/strings.xml b/testapps/transactionalVoipApp/res/values-hu/strings.xml
new file mode 100644
index 000000000..205404e75
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Tranzakciós API-teszttevékenység"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Hívás közbeni tranzakciós tevékenység"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefonáláshoz használt fiók regisztrálása"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Előtérben futó szolgáltatás indítása (gépi fordítás + alkalmazás szimulálása a háttérben)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Kimenő hívás indítása"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Bejövő hívás indítása"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"nincs beállítva hívásazonosító"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"válasz"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"leválasztás"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Fülhallgató"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Hangszóró"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"streamelés indítása"</string>
+ <string name="crash_app" msgid="2548690390730057704">"kivétel dobása"</string>
+ <string name="update_notification" msgid="8677916482672588779">"értesítés frissítése a folyamatban lévő hívás stílusára"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hy/strings.xml b/testapps/transactionalVoipApp/res/values-hy/strings.xml
new file mode 100644
index 000000000..85e6ae589
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hy/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Գործարքային API-ների փորձարկման գործողություն"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Գործարքներ զանգի ժամանակ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Հեռախոսի հաշվի գրանցում"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Գործարկել FGS-ը (ՄԹ-ի սիմուլացիա + հավելված ֆոնային ռեժիմում)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Սկսել ելքային զանգ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Սկսել մուտքային զանգ"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"զանգի նույնացուցիչ սահմանված չէ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"ակտիվացնել"</string>
+ <string name="answer" msgid="5423590397665409939">"պատասխանել"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"ապակտիվացնել"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"անջատել"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Լսափող"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Բարձրախոս"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"սկսել հեռարձակում"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ուղարկել հաղորդագրություն բացառության մասին"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ծանուցում ընթացիկ զանգի ոճի մասին"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-in/strings.xml b/testapps/transactionalVoipApp/res/values-in/strings.xml
new file mode 100644
index 000000000..935f03617
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-in/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktivitas pengujian API Transaksional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Aktivitas Transaksi Dalam Panggilan"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Daftarkan Akun Ponsel"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Mulai FGS (simulasikan MT + aplikasi di latar belakang)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Mulai Panggilan Keluar"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Mulai Panggilan Masuk"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"id panggilan tidak ditetapkan"</string>
+ <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="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>
+ <string name="start_stream" msgid="3567634786280097431">"mulai streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"tampilkan pengecualian"</string>
+ <string name="update_notification" msgid="8677916482672588779">"perbarui notifikasi ke gaya panggilan yang sedang berlangsung"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-is/strings.xml b/testapps/transactionalVoipApp/res/values-is/strings.xml
new file mode 100644
index 000000000..c0bcd2388
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-is/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Prófun á virkni forritaskila færslna"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Virkni í símtali"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Skrá símareikning"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Ræsa FGS (líkja eftir MT + forriti í bakgrunni)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Hefja hringt símtal"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Hefja símtal sem berst"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"númerabirting ekki stillt"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"svara"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"aftengja"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Eyrnatól"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Hátalari"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"hefja streymi"</string>
+ <string name="crash_app" msgid="2548690390730057704">"nota undantekningu"</string>
+ <string name="update_notification" msgid="8677916482672588779">"uppfæra tilkynningu í stíl símtals sem stendur yfir"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-it/strings.xml b/testapps/transactionalVoipApp/res/values-it/strings.xml
new file mode 100644
index 000000000..36a2816bd
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-it/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Attività di test dell\'API transazionale"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Attività di transazione durante la chiamata"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registra account telefono"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Avvia FGS (simulazione di MT + app in background)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Avvia chiamata in uscita"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Avvia chiamata in arrivo"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"id chiamata non impostato"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"risposta"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"disconnetti"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricolare"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altoparlante"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"avvia streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"genera eccezione"</string>
+ <string name="update_notification" msgid="8677916482672588779">"aggiorna la notifica allo stile di chiamata in corso"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-iw/strings.xml b/testapps/transactionalVoipApp/res/values-iw/strings.xml
new file mode 100644
index 000000000..3accc06d8
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-iw/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API test Activity"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"בר ביצוע בפעילות השיחה"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"רישום חשבון הטלפון"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"‏הפעלת FGS (סימולציה של MT + אפליקציה ברקע)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"התחלת שיחה יוצאת"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"התחלת שיחה נכנסת"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"מזהה השיחה לא הוגדר"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"הגדרה כפעיל"</string>
+ <string name="answer" msgid="5423590397665409939">"תשובה"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"הגדרה כלא פעיל"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ניתוק"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"אוזניה"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"רמקול"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"התחלת השידור"</string>
+ <string name="crash_app" msgid="2548690390730057704">"חריגה להקפצה של הודעת שגיאה"</string>
+ <string name="update_notification" msgid="8677916482672588779">"עדכון ההתראה לסגנון של שיחה רציפה"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ja/strings.xml b/testapps/transactionalVoipApp/res/values-ja/strings.xml
new file mode 100644
index 000000000..faaede609
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ja/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API テスト アクティビティ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transactional 通話アクティビティ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"スマートフォン アカウントを登録"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS を開始(MT + アプリをバックグラウンドでシミュレート)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"発信を開始"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"着信を開始"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"通話 ID が設定されていません"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"応答"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"切断"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"受話口"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"スピーカー"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ストリーミングを開始"</string>
+ <string name="crash_app" msgid="2548690390730057704">"例外をスロー"</string>
+ <string name="update_notification" msgid="8677916482672588779">"通話中スタイルへの通知を更新"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ka/strings.xml b/testapps/transactionalVoipApp/res/values-ka/strings.xml
new file mode 100644
index 000000000..6d94f3e01
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ka/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ტრანზაქციული API ტესტის აქტივობა"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ტრანზაქციის ზარის აქტივობა"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ტელეფონის ანგარიშის რეგისტრაცია"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS-ის დაწყება (MT + აპის სიმულაცია ფონზე)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"დაიწყეთ გამავალი ზარი"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"დაიწყეთ შემომავალი ზარი"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"აბონენტის ID არ არის დაყენებული"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"პასუხი"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"კავშირის გაწყვეტა"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ყურმილი"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"დინამიკი"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"სტრიმინგის დაწყება"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ხარვეზის გადასროლა"</string>
+ <string name="update_notification" msgid="8677916482672588779">"განაახლეთ შეტყობინება მიმდინარე ზარის სტილში"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-kk/strings.xml b/testapps/transactionalVoipApp/res/values-kk/strings.xml
new file mode 100644
index 000000000..03fd03129
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-kk/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Транзакциялық API сынағына қатысты әрекет"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Қоңыраулар тарихындағы транзакциялық қолданба"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Телефон аккаунтын тіркеу"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS-ті бастау (MT мен қолданбаны фонда симуляциялау)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Шығыс қоңырауын бастау"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Кіріс қоңырауын бастау"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"қоңырау идентификаторы орнатылмады"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"жауап беру"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ажырату"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Телефон динамигі"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Динамик"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"трансляцияны бастау"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ерекше жағдай туралы хабарлау"</string>
+ <string name="update_notification" msgid="8677916482672588779">"жүріп жатқан қоңырау стиліндегі хабарландыруды жаңату"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-km/strings.xml b/testapps/transactionalVoipApp/res/values-km/strings.xml
new file mode 100644
index 000000000..b3e45e408
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-km/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"សកម្មភាព​ធ្វើតេស្ត API ប្រតិបត្តិការ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"សកម្មភាពប្រតិបត្តិការ​នៅក្នុងការហៅទូរសព្ទ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ចុះឈ្មោះ​គណនី​ទូរសព្ទ"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"ចាប់ផ្ដើម FGS (ត្រាប់តាម MT + កម្មវិធី​នៅផ្ទៃខាងក្រោយ)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ចាប់ផ្ដើម​ការហៅចេញ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ចាប់ផ្ដើម​ការហៅ​ចូល"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"មិនបានកំណត់​លេខសម្គាល់​ការហៅទូរសព្ទទេ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"ឆ្លើយ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ផ្ដាច់"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ឧបករណ៍ស្ដាប់សំឡេង"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ឧបករណ៍​បំពង​សំឡេង"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ប៊្លូធូស"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ចាប់ផ្ដើម​ការផ្សាយ"</string>
+ <string name="crash_app" msgid="2548690390730057704">"បោះ​ការលើកលែង"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ធ្វើបច្ចុប្បន្នភាព​ការជូនដំណឹង​ចំពោះ​រចនាប័ទ្ម​នៃការហៅ​ទូរសព្ទ​ដែល​កំពុង​ដំណើរការ"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-kn/strings.xml b/testapps/transactionalVoipApp/res/values-kn/strings.xml
new file mode 100644
index 000000000..dd3fdd93f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-kn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ಟ್ರಾನ್ಸಾಕ್ಷನಲ್ API ಪರೀಕ್ಷಾ ಚಟುವಟಿಕೆ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ಕರೆ ಚಟುವಟಿಕೆಯಲ್ಲಿ ಟ್ರಾನ್ಸಾಕ್ಷನಲ್"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ಫೋನ್ ಖಾತೆಯನ್ನು ನೋಂದಾಯಿಸಿ"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ (MT + ಆ್ಯಪ್ ಅನ್ನು ಹಿನ್ನೆಲೆಯಲ್ಲಿ ಅನುಕರಿಸಿ)."</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ಹೊರಹೋಗುವ ಕರೆಯನ್ನು ಪ್ರಾರಂಭಿಸಿ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ಒಳಬರುವ ಕರೆಯನ್ನು ಪ್ರಾರಂಭಿಸಿ"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ಕರೆಮಾಡುವವರ ID ಅನ್ನು ಸೆಟ್ ಮಾಡಿಲ್ಲ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"ಉತ್ತರ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ಡಿಸ್‌ಕನೆಕ್ಟ್"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ಇಯರ್‌ಪೀಸ್‌"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ಸ್ಪೀಕರ್"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ಬ್ಲೂಟೂತ್"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ಸ್ಟ್ರೀಮ್ ಮಾಡುವುದನ್ನು ಪ್ರಾರಂಭಿಸಿ"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ಥ್ರೋ ಎಕ್ಸೆಪ್ಶನ್"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕರೆ ಶೈಲಿಗೆ ನೋಟಿಫಿಕೇಶನ್ ಅನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಿ"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ko/strings.xml b/testapps/transactionalVoipApp/res/values-ko/strings.xml
new file mode 100644
index 000000000..762dc9c45
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ko/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"트랜잭션 API 테스트 활동"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"통화 중 거래 활동"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"전화 계정 등록"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS 시작(MT 및 백그라운드 앱 시뮬레이션)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"발신 전화 시작"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"수신 전화 시작"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"통화 ID가 설정되지 않음"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"활성으로 설정"</string>
+ <string name="answer" msgid="5423590397665409939">"답변"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"비활성으로 설정"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"연결 해제"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"스피커"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"스피커"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"블루투스"</string>
+ <string name="start_stream" msgid="3567634786280097431">"스트리밍 시작"</string>
+ <string name="crash_app" msgid="2548690390730057704">"예외 발생"</string>
+ <string name="update_notification" msgid="8677916482672588779">"진행 중인 통화 스타일로 알림 업데이트"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ky/strings.xml b/testapps/transactionalVoipApp/res/values-ky/strings.xml
new file mode 100644
index 000000000..47422a029
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ky/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Транзакциялык API сыноосунун активдүүлүгү"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Чалуу учурундагы транзакциялар"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Телефон аккаунтун каттоо"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS\'ти иштетүү (фондо MT + колдонмону симуляциялоо)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Чыгуучу чалууну баштоо"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Кирүүчү чалууну баштоо"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"чалуунун идентификатору коюлган жок"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"жооп берүү"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ажыратуу"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Кулакчын"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Динамик"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"агымды баштоо"</string>
+ <string name="crash_app" msgid="2548690390730057704">"өзгөчө учурду түзүү"</string>
+ <string name="update_notification" msgid="8677916482672588779">"учурдагы чалуу үчүн жаңыртуу тууралуу билдирменин стили"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-lo/strings.xml b/testapps/transactionalVoipApp/res/values-lo/strings.xml
new file mode 100644
index 000000000..1e1d247f7
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-lo/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ກິດຈະກໍາການທົດສອບ API ທຸລະກໍາ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ການເຄື່ອນໄຫວຂອງທຸລະກຳລະຫວ່າງການໂທ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ລົງທະບຽນບັນຊີໂທລະສັບ"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"ເລີ່ມ FGS (ຈຳລອງ MT + ແອັບໃນພື້ນຫຼັງ)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ເລີ່ມສາຍໂທອອກ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ເລີ່ມສາຍໂທເຂົ້າ"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ບໍ່ໄດ້ຕັ້ງໝາຍເລກຜູ້ໂທ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"ຕັ້ງຄ່າເປັນນຳໃຊ້ຢູ່"</string>
+ <string name="answer" msgid="5423590397665409939">"ຄຳຕອບ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"ຕັ້ງຄ່າເປັນບໍ່ໄດ້ນຳໃຊ້"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ຕັດການເຊື່ອມຕໍ່"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ຫູຟັງ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ລຳໂພງ"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ເລີ່ມການສະຕຣີມ"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ຂໍ້ຍົກເວັ້ນໃນການໂຍນ"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ອັບເດດການແຈ້ງເຕືອນເປັນຮູບແບບການໂທທີ່ກຳລັງດຳເນີນການຢູ່"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-lt/strings.xml b/testapps/transactionalVoipApp/res/values-lt/strings.xml
new file mode 100644
index 000000000..88cd414ba
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-lt/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Operacijų API testavimo veikla"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Operacijų skambutyje veikla"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Užregistruoti telefono paskyrą"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Pradėti FGS (modeliuoti MT ir programą fone)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Pradėti siunčiamąjį skambutį"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Pradėti gaunamąjį skambutį"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"skambučio ID nenustatytas"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"atsakyti"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"atsijungti"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Garsiakalbis prie ausies"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Garsiakalbis"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"pradėti srautinį perdavimą"</string>
+ <string name="crash_app" msgid="2548690390730057704">"siųsti pranešimą apie išimtį"</string>
+ <string name="update_notification" msgid="8677916482672588779">"atnaujinti pranešimą į vykstančio skambučio stilių"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-lv/strings.xml b/testapps/transactionalVoipApp/res/values-lv/strings.xml
new file mode 100644
index 000000000..5e91ffee6
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-lv/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transakciju API testa darbība"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Ar darījumiem saistītas darbības zvana laikā"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Reģistrēt tālruņa kontu"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Sākt FGS (simulēt mašīntulkojumu un lietotni fonā)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Sākt izejoša zvana simulāciju"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Sākt ienākoša zvana simulāciju"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"zvana ID nav iestatīts"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"atbildēt"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"pārtraukt"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auss skaļrunis"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Skaļrunis"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"sākt straumēšanu"</string>
+ <string name="crash_app" msgid="2548690390730057704">"sūtīt ziņojumu par izņēmumu"</string>
+ <string name="update_notification" msgid="8677916482672588779">"atjaunināt paziņojumu atbilstoši pašreizējā zvana stilam"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-mk/strings.xml b/testapps/transactionalVoipApp/res/values-mk/strings.xml
new file mode 100644
index 000000000..d86879d3c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-mk/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Активност на тестирање на API за трансакции"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Трансакциска активност во повикот"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Регистрирај телефонска сметка"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Започни FGS (симулирај MT + апликација во заднина)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Започни појдовен повик"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Започни дојдовен повик"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"не е поставен ID на повикувач"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"одговори"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"прекини врска"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Слушалка"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Звучник"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"започни стриминг"</string>
+ <string name="crash_app" msgid="2548690390730057704">"отфрли исклучок"</string>
+ <string name="update_notification" msgid="8677916482672588779">"известување за ажурирање на стилот на тековниот повик"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ml/strings.xml b/testapps/transactionalVoipApp/res/values-ml/strings.xml
new file mode 100644
index 000000000..6c70b2228
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ml/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ട്രാൻസാക്ഷണൽ API ടെസ്റ്റ് ആക്റ്റിവിറ്റി"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ട്രാൻസാക്ഷണൽ ഇൻ കോൾ ആക്റ്റിവിറ്റി"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ഫോൺ അക്കൗണ്ട് രജിസ്റ്റർ ചെയ്യുക"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS ആരംഭിക്കുക (പശ്ചാത്തലത്തിൽ മെഷീൻ ട്രാൻസ്‌ലേഷൻ + ആപ്പ് സിമുലേറ്റ് ചെയ്യുക)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ഔട്ട്‌ഗോയിംഗ് കോൾ ആരംഭിക്കുക"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ഇൻകമിംഗ് കോൾ ആരംഭിക്കുക"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"കോൾ ഐഡി സജ്ജീകരിച്ചിട്ടില്ല"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"സജീവമെന്ന് സജ്ജീകരിക്കുക"</string>
+ <string name="answer" msgid="5423590397665409939">"ഉത്തരം നൽകുക"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"സജീവമല്ലെന്ന് സജ്ജീകരിക്കുക"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"വിച്ഛേദിക്കുക"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ഇയർഫോൺ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"സ്പീക്കർ"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"സ്‌ട്രീമിംഗ് ആരംഭിക്കുക"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ഒഴിവാക്കൽ ത്രോ ചെയ്യുക"</string>
+ <string name="update_notification" msgid="8677916482672588779">"സജീവമായ കോൾ ശൈലിയിലേക്ക് അറിയിപ്പ് അപ്ഡേറ്റ് ചെയ്യുക"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-mn/strings.xml b/testapps/transactionalVoipApp/res/values-mn/strings.xml
new file mode 100644
index 000000000..fecb956c5
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-mn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Гүйлгээний API-н туршилтын үйл ажиллагаа"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Дуудлагын үйл ажиллагааны гүйлгээ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Утасны бүртгэл бүртгүүлэх"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS-г эхлүүлэх (дэвсгэрт MT + аппыг загварчлах)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Залгасан дуудлагыг эхлүүлэх"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Ирсэн дуудлагыг эхлүүлэх"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"дуудлагын ID-г тохируулаагүй"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"хариулах"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"салгах"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Чихний спикер"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Чанга яригч"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"дамжуулалтыг эхлүүлэх"</string>
+ <string name="crash_app" msgid="2548690390730057704">"шидэх гажиг"</string>
+ <string name="update_notification" msgid="8677916482672588779">"үргэлжилж буй дуудлагын загварын шинэчлэлтийн мэдэгдэл"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-mr/strings.xml b/testapps/transactionalVoipApp/res/values-mr/strings.xml
new file mode 100644
index 000000000..97bf66580
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-mr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"व्यावहारिक API चाचणी अ‍ॅक्टिव्हिटी"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"कॉल अ‍ॅक्टिव्हिटी यामधील व्यवहार"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"फोन खात्याची नोंदणी करा"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS सुरू करा (बॅकग्राउंडमध्ये MT + अ‍ॅप सिम्युलेट करा)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"आउटगोइंग कॉल सुरू करा"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"इनकमिंग कॉल सुरू करा"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"कॉल आयडी सेट केलेला नाही"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"उत्तर"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"‍डिस्कनेक्ट करा"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"इअरपिस"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"स्पीकर"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ब्लूटूथ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"स्ट्रीम करणे सुरू करा"</string>
+ <string name="crash_app" msgid="2548690390730057704">"एक्सेप्शन जोडा"</string>
+ <string name="update_notification" msgid="8677916482672588779">"सुरू असलेल्या कॉल शैलीवर सूचना अपडेट करा"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ms/strings.xml b/testapps/transactionalVoipApp/res/values-ms/strings.xml
new file mode 100644
index 000000000..abcb70262
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ms/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktiviti ujian API transaksi"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaksi Aktiviti Dalam Panggilan"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Daftar Akaun Telefon"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Mulakan FGS (simulasi MT + apl pada latar)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Mulakan Panggilan Keluar"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Mulakan Panggilan Masuk"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID panggilan tidak ditetapkan"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"jawab"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"putuskan sambungan"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Alat dengar"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Pembesar suara"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"mulakan penstriman"</string>
+ <string name="crash_app" msgid="2548690390730057704">"buat pengecualian"</string>
+ <string name="update_notification" msgid="8677916482672588779">"kemas kinikan pemberitahuan kepada gaya panggilan keluar"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-my/strings.xml b/testapps/transactionalVoipApp/res/values-my/strings.xml
new file mode 100644
index 000000000..b8ee395f1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-my/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"အသိအမှတ်ပြုမှုဆိုင်ရာ API စမ်းသပ်လုပ်ဆောင်ချက်"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ခေါ်ဆိုမှုလုပ်ဆောင်ချက်ရှိ မှတ်တမ်းဆိုင်ရာ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ဖုန်းအကောင့် မှတ်ပုံတင်ရန်"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS (အသွင်တူ MT + နောက်ခံရှိ အက်ပ်) စတင်ရန်"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"အထွက် ခေါ်ဆိုမှု စတင်ရန်"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"အဝင်ခေါ်ဆိုမှု စတင်ရန်"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ခေါ်ဆိုမှု id သတ်မှတ်မထားပါ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"ပြောနေသည်ဟု သတ်မှတ်ရန်"</string>
+ <string name="answer" msgid="5423590397665409939">"ဖြေကြားရန်"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"ပြောမနေပါဟု သတ်မှတ်ရန်"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ချိတ်ဆက်မှုဖြုတ်ရန်"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"တယ်လီဖုန်းနားခွက်"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"စပီကာ"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ဘလူးတုသ်"</string>
+ <string name="start_stream" msgid="3567634786280097431">"တိုက်ရိုက်လွှင့်ခြင်း စတင်ရန်"</string>
+ <string name="crash_app" msgid="2548690390730057704">"throw exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"လက်ရှိခေါ်ဆိုမှုပုံစံအတွက် အပ်ဒိတ်အကြောင်းကြားချက်"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-nb/strings.xml b/testapps/transactionalVoipApp/res/values-nb/strings.xml
new file mode 100644
index 000000000..22bb06fc6
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-nb/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Testaktivitet for Transactional API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaksjonell i samtale-aktivitet"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrer telefonkonto"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Start FGS (simuler MT + app i bakgrunnen)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Start utgående anrop"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Start innkommende anrop"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"anrops-ID er ikke angitt"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"svar"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"koble fra"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Ørehøyttaler"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Høyttaler"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"start strømming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"unntak – avbryt med en feil"</string>
+ <string name="update_notification" msgid="8677916482672588779">"oppdater varslingsstil til «Pågående anrop»"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ne/strings.xml b/testapps/transactionalVoipApp/res/values-ne/strings.xml
new file mode 100644
index 000000000..e9bc805fd
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ne/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API को परीक्षणसम्बन्धी गतिविधि"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"कलमा क्लाइन्ट र सर्भरबिच गरिएको कुराकानीसम्बन्धी क्रियाकलाप"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"फोन खाता दर्ता गर्नुहोस्"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS सुरु गर्नुहोस् (ब्याकग्राउन्डमा MT + एप सिमुलेट गर्नुहोस्)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"बहिर्गमन कल सुरु गर्नुहोस्"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"आगमन कल सुरु गर्नुहोस्"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"कल ID सेट गरिएको छैन"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"कल उठाउनुहोस्"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"डिस्कनेक्ट गर्नुहोस्"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"इयरपिस"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"स्पिकर"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ब्लुटुथ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"स्ट्रिम गर्न थाल्नुहोस्"</string>
+ <string name="crash_app" msgid="2548690390730057704">"अपवाद देखाउने काम"</string>
+ <string name="update_notification" msgid="8677916482672588779">"कल गरिरहेका बेला सूचना जुन शैलीमा देखिन्छ सोही शैली प्रयोग गर्नुहोस्"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-nl/strings.xml b/testapps/transactionalVoipApp/res/values-nl/strings.xml
new file mode 100644
index 000000000..1ba3f9cef
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-nl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Testactiviteit Transactional API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Beveiligd gesprek"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefoonaccount registreren"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Service op de voorgrond (FGS) starten (MT + app op de achtergrond simuleren)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Uitgaand gesprek starten"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Inkomend gesprek starten"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"Beller-ID niet ingesteld"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"antwoord"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"loskoppelen"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Oortelefoon"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Speaker"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"streamen starten"</string>
+ <string name="crash_app" msgid="2548690390730057704">"uitzondering activeren"</string>
+ <string name="update_notification" msgid="8677916482672588779">"updatemelding naar actieve gespreksstijl"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-or/strings.xml b/testapps/transactionalVoipApp/res/values-or/strings.xml
new file mode 100644
index 000000000..f3391ea3e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-or/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ଟ୍ରାଞ୍ଜେକସନାଲ API ପରୀକ୍ଷଣର କାର୍ଯ୍ୟକଳାପ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ଟ୍ରାଞ୍ଜେକସନାଲ ଇନ କଲ କାର୍ଯ୍ୟକଳାପ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ଫୋନ ଆକାଉଣ୍ଟର ପଞ୍ଜିକରଣ କରନ୍ତୁ"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS ଆରମ୍ଭ କରନ୍ତୁ (ପୃଷ୍ଠପଟରେ MT + ଆପକୁ ସିମୁଲେଟ କରନ୍ତୁ)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ଆଉଟଗୋଇଂ କଲ ଆରମ୍ଭ କରନ୍ତୁ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ଇନକମିଂ କଲ ଆରମ୍ଭ କରନ୍ତୁ"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"କଲ ID ସେଟ କରାଯାଇନାହିଁ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"ଉତ୍ତର ଦିଅନ୍ତୁ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ଡିସକନେକ୍ଟ କରନ୍ତୁ"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ଇୟରପିସ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ସ୍ପିକର"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ବ୍ଲୁଟୁଥ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ଷ୍ଟ୍ରିମିଂ ଆରମ୍ଭ କରନ୍ତୁ"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ଥ୍ରୋ ଏକ୍ସସେପସନ"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ଚାଲିଥିବା କଲ ଷ୍ଟାଇଲ ପାଇଁ ବିଜ୍ଞପ୍ତିକୁ ଅପଡେଟ କରନ୍ତୁ"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pa/strings.xml b/testapps/transactionalVoipApp/res/values-pa/strings.xml
new file mode 100644
index 000000000..76e367d7e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pa/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ਲੈਣ-ਦੇਣ API ਜਾਂਚ ਸਰਗਰਮੀ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ਲੈਣ-ਦੇਣ ਸੰਬੰਧੀ ਇਨ-ਕਾਲ ਸਰਗਰਮੀ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ਫ਼ੋਨ ਖਾਤਾ ਰਜਿਸਟਰ ਕਰੋ"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS ਸ਼ੁਰੂ ਕਰੋ (ਬੈਕਗ੍ਰਾਊਂਡ ਵਿੱਚ MT + ਐਪ ਨੂੰ ਸਿਮੂਲੇਟ ਕਰੋ)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"ਆਊਟਗੋਇੰਗ ਕਾਲ ਸ਼ੁਰੂ ਕਰੋ"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ਇਨਕਮਿੰਗ ਕਾਲ ਸ਼ੁਰੂ ਕਰੋ"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ਕਾਲਰ ਆਈਡੀ ਸੈੱਟ ਨਹੀਂ ਹੈ"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"ਜਵਾਬ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ਡਿਸਕਨੈਕਟ ਕਰੋ"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ਈਯਰਪੀਸ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ਸਪੀਕਰ"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"ਬਲੂਟੁੱਥ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ਸਟ੍ਰੀਮਿੰਗ ਸ਼ੁਰੂ ਕਰੋ"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ਅਪਵਾਦ ਸ਼ਾਮਲ ਕਰੋ"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ਜਾਰੀ ਕਾਲ ਸਟਾਈਲ \'ਤੇ ਸੂਚਨਾ ਅੱਪਡੇਟ ਕਰੋ"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pl/strings.xml b/testapps/transactionalVoipApp/res/values-pl/strings.xml
new file mode 100644
index 000000000..c6115b841
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Czynność testowa dotycząca transakcji związanej z interfejsem API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Aktywność transakcyjna w trakcie rozmowy"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Zarejestruj konto telefonu"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Uruchom FGS (symulacja MT + aplikacja w tle)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Rozpocznij połączenie wychodzące"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Rozpocznij połączenie przychodzące"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"nie ustawiono ID rozmówcy"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"ustawAktywny"</string>
+ <string name="answer" msgid="5423590397665409939">"odpowiedź"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"ustawNieaktywny"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"rozłącz"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Słuchawka"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Głośnik"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"rozpocznij transmisję"</string>
+ <string name="crash_app" msgid="2548690390730057704">"wyjątek dotyczący zgłoszenia"</string>
+ <string name="update_notification" msgid="8677916482672588779">"zaktualizuj powiadomienie do stylu trwającej rozmowy"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pt-rPT/strings.xml b/testapps/transactionalVoipApp/res/values-pt-rPT/strings.xml
new file mode 100644
index 000000000..a5b3ea050
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pt-rPT/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Atividade de teste da API transacional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transacional na atividade da chamada"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registar conta do telemóvel"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Iniciar FGS (simular TA + app em segundo plano)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Iniciar chamada feita"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Iniciar chamada recebida"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID da chamada não definido"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"atender"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"desligar"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altifalante"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Iniciar stream"</string>
+ <string name="crash_app" msgid="2548690390730057704">"acionar exceção"</string>
+ <string name="update_notification" msgid="8677916482672588779">"atualizar estilo de notificação para chamada em curso"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pt/strings.xml b/testapps/transactionalVoipApp/res/values-pt/strings.xml
new file mode 100644
index 000000000..a09c64d9f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pt/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Atividade de teste da API transacional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Atividade em chamadas transacionais"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrar conta telefônica"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Iniciar FGS (simular MT + app em segundo plano)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Iniciar ligação efetuada"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Iniciar ligação recebida"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"identificador de chamadas não definido"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"resposta"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"desconectar"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Minifone de ouvido"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Alto-falante"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Iniciar transmissão"</string>
+ <string name="crash_app" msgid="2548690390730057704">"gerar exceção"</string>
+ <string name="update_notification" msgid="8677916482672588779">"notificação de atualização para o estilo \"Chamada em andamento\""</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ro/strings.xml b/testapps/transactionalVoipApp/res/values-ro/strings.xml
new file mode 100644
index 000000000..261a5adcd
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ro/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Activitate de testare a API-ului tranzacțional"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Activitate tranzacțională în timpul apelului"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Înregistrează contul de telefon"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Pornește FGS (simulează MT + aplicația în fundal)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Inițiază un apel efectuat"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Inițiază un apel primit"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID-ul apelului nu este setat"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"răspuns"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"deconectează"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Cască"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Difuzor"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"începe streamingul"</string>
+ <string name="crash_app" msgid="2548690390730057704">"trimite excepție"</string>
+ <string name="update_notification" msgid="8677916482672588779">"actualizează notificarea la stilul de apel în desfășurare"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ru/strings.xml b/testapps/transactionalVoipApp/res/values-ru/strings.xml
new file mode 100644
index 000000000..c05e7ea88
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ru/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Активность тестирования API транзакций"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Транзакции во время вызовов"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Зарегистрировать аккаунт телефона"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Запустить активную службу (симуляция МП + приложение в фоновом режиме)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Начать исходящий вызов"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Начать входящий вызов"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"идентификатор вызова не задан"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Активировать"</string>
+ <string name="answer" msgid="5423590397665409939">"ответить"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Деактивировать"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"разъединить"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Динамик телефона"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Колонка"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Начать трансляцию"</string>
+ <string name="crash_app" msgid="2548690390730057704">"отправить сообщение об исключении"</string>
+ <string name="update_notification" msgid="8677916482672588779">"стиль уведомления об обновлении для текущего звонка"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-si/strings.xml b/testapps/transactionalVoipApp/res/values-si/strings.xml
new file mode 100644
index 000000000..d8b8a6ffa
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-si/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"ගනුදෙනු API පරීක්ෂණ ක්‍රියාකාරකම්"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"ඇමතුම් ක්‍රියාකාරකම්වල ගනුදෙනු"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"දුරකථන ගිණුම ලියාපදිංචි කරන්න"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS අරඹන්න (පසුබිමේ MT + යෙදුම අනුකරණය කරන්න)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"පිටතට යන ඇමතුම අරඹන්න"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"එන ඇමතුම අරඹන්න"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"අමතුම්කරුගේ id සකසා නැත"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"පිළිතුරු දෙන්න"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"විසන්ධි කරන්න"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"සවන් කඩ"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ස්පීකරය"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"බ්ලූටූත්"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ප්‍රවාහය අරඹන්න"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ව්‍යතිරේකය දමන්න"</string>
+ <string name="update_notification" msgid="8677916482672588779">"පවතින ඇමතුම් විලාසයට යාවත්කාලීනයේ දැනුම්දීම"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sk/strings.xml b/testapps/transactionalVoipApp/res/values-sk/strings.xml
new file mode 100644
index 000000000..38478827b
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sk/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Testovacia aktivita transakčného rozhrania API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transakčná aktivita počas hovoru"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrovať telefónny účet"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Spustiť FGS (simulácia MT a aplikácie na pozadí)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Začať odchádzajúci hovor"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Začať prichádzajúci hovor"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"identifikátor hovoru nie je nastavený"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"prijať"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"odpojiť"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Slúchadlo"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Reproduktor"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"spustiť streamovanie"</string>
+ <string name="crash_app" msgid="2548690390730057704">"vyvolať výnimku"</string>
+ <string name="update_notification" msgid="8677916482672588779">"aktualizovať upozornenie na štýl prebiehajúceho hovoru"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sl/strings.xml b/testapps/transactionalVoipApp/res/values-sl/strings.xml
new file mode 100644
index 000000000..dec36222e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Preizkusna dejavnost transakcijskega API-ja"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transakcijska dejavnost v klicu"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registracija telefonskega računa"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Zaženi FGS (simuliraj strojni prevod + aplikacijo v ozadju)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Začni odhodni klic"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Začni dohodni klic"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"id klica ni nastavljen"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Nastavi kot aktivno"</string>
+ <string name="answer" msgid="5423590397665409939">"sprejmi"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Nastavi kot neaktivno"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"prekini klic"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Slušalka"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Zvočnik"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"začni pretočno predvajanje"</string>
+ <string name="crash_app" msgid="2548690390730057704">"sprožitev izjeme"</string>
+ <string name="update_notification" msgid="8677916482672588779">"posodobi obvestilo na slog trenutnega klica"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sq/strings.xml b/testapps/transactionalVoipApp/res/values-sq/strings.xml
new file mode 100644
index 000000000..ddaba66bd
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sq/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktiviteti i testimit të API-së së transaksioneve"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Aktivitet transaksioni brenda telefonatës"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Regjistro llogarinë e telefonit"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Nis shërbimin FGS (simulo përkthimin kompjuterik dhe aplikacionin në sfond)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Nis një telefonatë dalëse"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Nis një telefonatë hyrëse"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ID-ja e telefonatës nuk është caktuar"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"përgjigju"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <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="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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sr/strings.xml b/testapps/transactionalVoipApp/res/values-sr/strings.xml
new file mode 100644
index 000000000..cd413f41f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Активност тестирања трансакционог API-ја"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Активност позива у вези са трансакцијама"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Региструј налог телефона"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Покрени FGS (симулирајте MT + апликацију у позадини)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Започните одлазни позив"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Започните долазни позив"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ИД позива није подешен"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"одговори"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"прекини везу"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Слушалица"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Звучник"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"почните да стримујете"</string>
+ <string name="crash_app" msgid="2548690390730057704">"избацити изузетак"</string>
+ <string name="update_notification" msgid="8677916482672588779">"ажурирајте обавештење на стил актуелног позива"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sv/strings.xml b/testapps/transactionalVoipApp/res/values-sv/strings.xml
new file mode 100644
index 000000000..f74b775da
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sv/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktiviteten Test av transaktions-API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaktioner i samtalsaktivitet"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Registrera telefonkonto"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Starta FGS (simulera MT + app i bakgrunden)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Starta utgående samtal"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Starta inkommande samtal"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"nummerpresentatör inte inställd"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"svara"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"koppla från"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Lur"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Högtalare"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"starta streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"utlös undantag"</string>
+ <string name="update_notification" msgid="8677916482672588779">"uppdatera avisering till format för pågående samtal"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sw/strings.xml b/testapps/transactionalVoipApp/res/values-sw/strings.xml
new file mode 100644
index 000000000..b7d0d0f70
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sw/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Shughuli za jaribio la API ya Uthibitishaji"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Shughuli ya Muamala Kwenye Simu"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Sajili Akaunti ya Simu"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Anzisha FGS (kuiga Tafsiri ya Mashine na programu katika hali ya chinichini)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Anzisha Uigaji wa Simu Unayopiga"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Anzisha Uigaji wa Simu Uliyopigiwa"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"kitambulisho cha anayepiga hakijawekwa"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"jibu"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ondoa"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Spika ya sikioni"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Spika"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"anzisha kutiririsha"</string>
+ <string name="crash_app" msgid="2548690390730057704">"hitilafu wakati wa kutekeleza programu"</string>
+ <string name="update_notification" msgid="8677916482672588779">"sasisha arifa kwenye mtindo wa simu inayoendelea"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ta/strings.xml b/testapps/transactionalVoipApp/res/values-ta/strings.xml
new file mode 100644
index 000000000..39b410a30
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ta/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API சோதனை செயல்பாடு"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"டிரான்சாக்‌ஷனல் இன் கால் ஆக்டிவிட்டி"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"மொபைல் கணக்கைப் பதிவுசெய்தல்"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGSஸைத் தொடங்கு (MT + ஆப்ஸைப் பின்னணியில் சிமுலேட் செய்)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"வெளிச்செல்லும் அழைப்பைத் தொடங்கு"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"உள்வரும் அழைப்பைத் தொடங்கு"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"அழைப்பு ஐடி அமைக்கப்படவில்லை"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"செயலில் அமை"</string>
+ <string name="answer" msgid="5423590397665409939">"பதில்"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"செயலற்ற நிலையில் அமை"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"துண்டி"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ஒலி கேட்கும் பகுதி"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ஸ்பீக்கர்"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"புளூடூத்"</string>
+ <string name="start_stream" msgid="3567634786280097431">"ஸ்ட்ரீமிங்கைத் தொடங்கு"</string>
+ <string name="crash_app" msgid="2548690390730057704">"விதிவிலக்கைத் தொடங்கு"</string>
+ <string name="update_notification" msgid="8677916482672588779">"செயலில் உள்ள அழைப்பு ஸ்டைலுக்கான அறிவிப்பைப் புதுப்பிக்கவும்"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-te/strings.xml b/testapps/transactionalVoipApp/res/values-te/strings.xml
new file mode 100644
index 000000000..f4560ab17
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-te/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"లావాదేవీల API టెస్ట్ యాక్టివిటీ"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"కాల్ యాక్టివిటీలో లావాదేవీ"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ఫోన్ ఖాతాను రిజిస్టర్ చేయండి"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS (అనుకరణ MT + బ్యాక్‌గ్రౌండ్‌లో యాప్)ను ప్రారంభించండి"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"అవుట్‌గోయింగ్ కాల్‌ను ప్రారంభించండి"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"ఇన్‌కమింగ్ కాల్‌ను ప్రారంభించండి"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"కాల్ id సెట్ చేయబడలేదు"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"సమాధానం"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"డిస్‌కనెక్ట్ చేయండి"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ఇయర్‌పీస్"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"స్పీకర్"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"బ్లూటూత్"</string>
+ <string name="start_stream" msgid="3567634786280097431">"స్ట్రీమింగ్‌ను ప్రారంభించండి"</string>
+ <string name="crash_app" msgid="2548690390730057704">"మినహాయింపు వేయండి"</string>
+ <string name="update_notification" msgid="8677916482672588779">"జరుగుతున్న కాల్ స్టయిల్‌కి నోటిఫికేషన్‌ను అప్‌డేట్ చేయండి"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-th/strings.xml b/testapps/transactionalVoipApp/res/values-th/strings.xml
new file mode 100644
index 000000000..545110b22
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-th/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"กิจกรรมการทดสอบ API ธุรกรรม"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"กิจกรรมธุรกรรมระหว่างการโทร"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"ลงทะเบียนบัญชีของโทรศัพท์"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"เริ่ม FGS (จําลอง MT + แอปในพื้นหลัง)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"เริ่มสายโทรออก"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"เริ่มสายเรียกเข้า"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ไม่ได้ตั้งค่าหมายเลขผู้โทร"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"ตั้งค่าเป็นใช้งานอยู่"</string>
+ <string name="answer" msgid="5423590397665409939">"คำตอบ"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"ตั้งค่าเป็นไม่ใช้งาน"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ยกเลิกการเชื่อมต่อ"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"หูฟังโทรศัพท์"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"ลำโพง"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"บลูทูธ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"เริ่มสตรีมมิง"</string>
+ <string name="crash_app" msgid="2548690390730057704">"ส่งข้อยกเว้น"</string>
+ <string name="update_notification" msgid="8677916482672588779">"อัปเดตการแจ้งเตือนไปยังรูปแบบการโทรที่ดำเนินอยู่"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-tl/strings.xml b/testapps/transactionalVoipApp/res/values-tl/strings.xml
new file mode 100644
index 000000000..6cc2a2b22
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-tl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Aktibidad ng pansubok na Transactional API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Transaksyonal na In Call na Aktibidad"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Irehistro ang Phone Account"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Simulan ang FGS (i-simulate ang MT + app sa background)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Magsimula ng Papalabas na Tawag"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Magsimula ng Papasok na Tawag"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"hindi naitakda ang call id"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"sagutin"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"idiskonekta"</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>
+ <string name="start_stream" msgid="3567634786280097431">"simulan ang streaming"</string>
+ <string name="crash_app" msgid="2548690390730057704">"throw exception"</string>
+ <string name="update_notification" msgid="8677916482672588779">"i-update ang notification sa istilo ng kasalukuyang tawag"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-tr/strings.xml b/testapps/transactionalVoipApp/res/values-tr/strings.xml
new file mode 100644
index 000000000..ec230481f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-tr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API test etkinliği"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Görüşme İçin İşlem Etkinliği"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefon Hesabını Kaydet"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Ön plan hizmetlerini (FGS) başlat (makine çevirisi + arka plandaki uygulamayı simüle et)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Giden Arama Başlat"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Gelen Arama Başlat"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"arama kimliği ayarlanmadı"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"yanıtla"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"bağlantıyı kes"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Kulaklık"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Hoparlör"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"yayın başlat"</string>
+ <string name="crash_app" msgid="2548690390730057704">"istisna gönder"</string>
+ <string name="update_notification" msgid="8677916482672588779">"bildirimi devam eden arama stiline güncelle"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-uk/strings.xml b/testapps/transactionalVoipApp/res/values-uk/strings.xml
new file mode 100644
index 000000000..0069f3d7f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-uk/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Тестування API підтвердження"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Трансакції під час викликів"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Зареєструвати обліковий запис телефона"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Запустити активний сервіс (симуляція МП + додаток у фоновому режимі)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Почати вихідний виклик"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Почати вхідний виклик"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"ідентифікатор виклику не налаштовано"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"позначити як активний"</string>
+ <string name="answer" msgid="5423590397665409939">"відповідь"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"позначити як неактивний"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"від’єднати"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Динамік"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Колонка"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Почати трансляцію"</string>
+ <string name="crash_app" msgid="2548690390730057704">"надіслати повідомлення про виняток"</string>
+ <string name="update_notification" msgid="8677916482672588779">"стиль сповіщення про оновлення для поточного дзвінка"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ur/strings.xml b/testapps/transactionalVoipApp/res/values-ur/strings.xml
new file mode 100644
index 000000000..e41027ad8
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ur/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"‏ٹرانزیکشنل API ٹیسٹ کی سرگرمی"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"کال کی سرگرمی میں ٹرانزیکشنل"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"فون کے اکاؤنٹ کو رجسٹر کریں"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"‏FGS شروع کریں ( بیک گراؤنڈ میں MT +‎ ایپ کی نقل کریں)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"آؤٹ گوئنگ کال شروع کریں"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"اِن کمنگ کال شروع کریں"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"‏کال ID سیٹ نہیں ہے"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"فعال پر سیٹ کریں"</string>
+ <string name="answer" msgid="5423590397665409939">"جواب"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"غیر فعال پر سیٹ کریں"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"غیر منسلک کریں"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"ایئر پیس"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"اسپیکر"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"بلوٹوتھ"</string>
+ <string name="start_stream" msgid="3567634786280097431">"سلسلہ بندی شروع کریں"</string>
+ <string name="crash_app" msgid="2548690390730057704">"تھرو ایکسیپشن"</string>
+ <string name="update_notification" msgid="8677916482672588779">"اطلاع کو جاری کال طرز پر اپ ڈیٹ کریں"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-uz/strings.xml b/testapps/transactionalVoipApp/res/values-uz/strings.xml
new file mode 100644
index 000000000..faa0b4bdc
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-uz/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Tranzaksiyaviy API sinovi faoliyati"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Chaqiruvda tranzaksiya faoliyati"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Telefon hisobini ro‘yxatdan o‘tkazish"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"FGS boshlash (MT + fonda ilova simulyatsiyasi)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Chiquvchi chaqiruvni boshlash"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Kiruvchi chaqiruvni boshlash"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"chaqiruv id belgilanmagan"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"javob berish"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"uzish"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Quloq karnaychasi"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Karnay"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"strimingni boshlash"</string>
+ <string name="crash_app" msgid="2548690390730057704">"istisno berish"</string>
+ <string name="update_notification" msgid="8677916482672588779">"bildirishnomani joriy chaqiruv uslubida yangilash"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-vi/strings.xml b/testapps/transactionalVoipApp/res/values-vi/strings.xml
new file mode 100644
index 000000000..a54d54408
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-vi/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Hoạt động kiểm tra cho API Xác nhận trao đổi"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Hoạt động giao dịch trong cuộc gọi"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Đăng ký tài khoản điện thoại"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Khởi động FGS (mô phỏng MT + ứng dụng trong nền)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Bắt đầu cuộc gọi đi"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Bắt đầu cuộc gọi đến"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"chưa đặt mã cuộc gọi"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"Đặt thành đang hoạt động"</string>
+ <string name="answer" msgid="5423590397665409939">"trả lời"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"Đặt thành không hoạt động"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"ngắt kết nối"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Loa tai nghe"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Loa"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"bắt đầu phát trực tuyến"</string>
+ <string name="crash_app" msgid="2548690390730057704">"đưa ra trường hợp ngoại lệ"</string>
+ <string name="update_notification" msgid="8677916482672588779">"cập nhật thông báo thành kiểu cuộc gọi đang diễn ra"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml b/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml
new file mode 100644
index 000000000..a74cbb539
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"事务性 API 测试活动"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"通话活动中的事务"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"注册电话账号"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"启动 FGS(在后台模拟 MT + 应用)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"开始去电"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"开始来电"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"未设置来电显示/本机号码"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"回复"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"断开连接"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"手机听筒"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"扬声器"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"蓝牙"</string>
+ <string name="start_stream" msgid="3567634786280097431">"开始直播"</string>
+ <string name="crash_app" msgid="2548690390730057704">"抛出异常"</string>
+ <string name="update_notification" msgid="8677916482672588779">"将通知更新为当前通话样式"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-zh-rHK/strings.xml b/testapps/transactionalVoipApp/res/values-zh-rHK/strings.xml
new file mode 100644
index 000000000..e00caa948
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zh-rHK/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Transactional API 測試活動"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"交易來電活動"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"註冊電話帳戶"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"開始 FGS (模擬 MT + 背景應用程式)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"開始撥出電話"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"開始來電"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"未設定來電顯示"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"設為使用中"</string>
+ <string name="answer" msgid="5423590397665409939">"接聽"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"設為停用"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"解除連結"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"聽筒"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"喇叭"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"藍牙"</string>
+ <string name="start_stream" msgid="3567634786280097431">"開始串流播放"</string>
+ <string name="crash_app" msgid="2548690390730057704">"擲回例外狀況"</string>
+ <string name="update_notification" msgid="8677916482672588779">"更新通知至通話中樣式"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-zh-rTW/strings.xml b/testapps/transactionalVoipApp/res/values-zh-rTW/strings.xml
new file mode 100644
index 000000000..1a6da94d7
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zh-rTW/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"交易 API 測試活動"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"通話活動交易資訊"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"註冊電話帳戶"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"啟動 FGS (在背景模擬機器翻譯和應用程式)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"開始模擬撥出電話"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"開始模擬來電"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"未設定通話 ID"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"設為使用中"</string>
+ <string name="answer" msgid="5423590397665409939">"接聽"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"設為閒置"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"掛斷"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"耳機"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"喇叭"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"藍牙"</string>
+ <string name="start_stream" msgid="3567634786280097431">"開始串流播放"</string>
+ <string name="crash_app" msgid="2548690390730057704">"擲回例外狀況"</string>
+ <string name="update_notification" msgid="8677916482672588779">"將通知更新為通話中樣式"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-zu/strings.xml b/testapps/transactionalVoipApp/res/values-zu/strings.xml
new file mode 100644
index 000000000..cd86e811c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="2907804426411305091">"Umsebenzi wokuhlolwa kwe-Transactional API"</string>
+ <string name="in_call_activity_name" msgid="7545884666442897585">"Okwenziwayo Emsebenzini Wekholi"</string>
+ <string name="register_phone_account" msgid="1920315963082350332">"Bhalisa I-akhawunti Yefoni"</string>
+ <string name="start_foreground_service" msgid="8968755699895128574">"Qala ama-FGS (lingisa i-app ye-MT + ngemuva)"</string>
+ <string name="start_outgoing" msgid="1441644037370361864">"Qala ikholi ephumela ngaphandle"</string>
+ <string name="start_incoming" msgid="6444983300186361271">"Qala Ikholi Engenayo"</string>
+ <string name="get_call_id" msgid="5513943242738347108">"I-ID yekholi ayisethiwe"</string>
+ <string name="set_call_active" msgid="3365404393507589899">"I-setActive"</string>
+ <string name="answer" msgid="5423590397665409939">"impendulo"</string>
+ <string name="set_call_inactive" msgid="7106775211368705195">"I-setInactive"</string>
+ <string name="disconnect_call" msgid="1349412380315371385">"nqamula"</string>
+ <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Isipikha sendlebe"</string>
+ <string name="request_speaker_endpoint" msgid="1033259535289845405">"Isipikha"</string>
+ <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"I-Bluetooth"</string>
+ <string name="start_stream" msgid="3567634786280097431">"Qala ukusakaza-bukhoma"</string>
+ <string name="crash_app" msgid="2548690390730057704">"phonsela okuhlukile"</string>
+ <string name="update_notification" msgid="8677916482672588779">"buyekeza isaziso kusitayela sekholi eqhubekayo"</string>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values/strings.xml b/testapps/transactionalVoipApp/res/values/strings.xml
new file mode 100644
index 000000000..8239a0e7f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources>
+ <string name="app_name">Transactional API test Activity</string>
+ <string name="in_call_activity_name">Transactional In Call Activity</string>
+
+ <!-- Main Activity -->
+ <string name="register_phone_account">Register Phone Account</string>
+ <string name="start_foreground_service">Start FGS (simulate MT + app in background)</string>
+ <string name="start_outgoing">Start Outgoing Call</string>
+ <string name="start_incoming">Start Incoming Call</string>
+
+ <!-- InCall Activity -->
+ <string name="get_call_id">call id not set</string>
+ <!-- control the call state -->
+ <string name="set_call_active">setActive</string>
+ <string name="answer">answer</string>
+ <string name="set_call_inactive">setInactive</string>
+ <string name="disconnect_call">disconnect</string>
+ <!-- control the call audio -->
+ <string name="request_earpiece_endpoint">Earpiece</string>
+ <string name="request_speaker_endpoint">Speaker</string>
+ <string name="request_bluetooth_endpoint">Bluetooth</string>
+ <!-- extra functionality -->
+ <string name="start_stream">start streaming</string>
+ <string name="crash_app">throw exception</string>
+ <string name="update_notification"> update notification to ongoing call style</string>
+
+</resources> \ No newline at end of file
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java
new file mode 100644
index 000000000..b503e948b
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java
@@ -0,0 +1,76 @@
+/*
+ * 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.transactionalVoipApp;
+
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+public class BackgroundIncomingCallService extends Service {
+ // finals
+ private static final String TAG = "BackgroundIncomingCallService";
+ // instance vars
+ private NotificationManager mNotificationManager;
+ private final IBinder mBinder = new LocalBinder();
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "onCreate");
+ mNotificationManager = getSystemService(NotificationManager.class);
+ }
+
+ @Override
+ @StartResult
+ public int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
+ Log.i(TAG, String.format("onStartCommand: intent=[%s]", intent));
+
+ // create the notification channel
+ if (mNotificationManager != null) {
+ mNotificationManager.createNotificationChannel(new NotificationChannel(
+ Utils.CHANNEL_ID, "incoming calls", NotificationManager.IMPORTANCE_DEFAULT));
+ }
+
+ // start the foreground service and post a notification
+ startForeground(98765, Utils.createCallStyleNotification(this),
+ FOREGROUND_SERVICE_TYPE_PHONE_CALL);
+
+ return Service.START_STICKY_COMPATIBILITY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, String.format("onBind: intent=[%s]", intent));
+ return mBinder;
+ }
+
+ /**
+ * Class used for the client Binder. Because we know this service always
+ * runs in the same process as its clients, we don't need to deal with IPC.
+ */
+ public class LocalBinder extends Binder {
+ BackgroundIncomingCallService getService() {
+ // Return this instance of LocalService so clients can call public methods
+ return BackgroundIncomingCallService.this;
+ }
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
new file mode 100644
index 000000000..50556a1b2
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -0,0 +1,303 @@
+/*
+ * 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.transactionalVoipApp;
+
+import static android.telecom.CallAttributes.AUDIO_CALL;
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
+
+import android.app.Activity;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallEndpoint;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+public class InCallActivity extends Activity {
+ private static final String TAG = "InCallActivity";
+ private final AudioManager.AudioRecordingCallback mAudioRecordingCallback =
+ Utils.getAudioRecordingCallback();
+ private static TelecomManager mTelecomManager;
+ private MyVoipCall mVoipCall;
+ private MediaPlayer mMediaPlayer;
+ private AudioRecord mAudioRecord;
+ private int mCallDirection = DIRECTION_INCOMING;
+ private TextView mCurrentEndpointTextView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "#onCreate: in function");
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.in_call_activity);
+
+ Bundle extras = getIntent().getExtras();
+ // Copy the extras with properties like call direction into the extras so the below
+ // code can access them.
+ if (extras != null && extras.containsKey(Utils.sEXTRAS_KEY)) {
+ extras.putAll(extras.getBundle(Utils.sEXTRAS_KEY));
+ }
+ if (extras != null) {
+ mCallDirection = extras.getInt(Utils.sCALL_DIRECTION_KEY, DIRECTION_INCOMING);
+ }
+ mCurrentEndpointTextView = findViewById(R.id.current_endpoint);
+ mCurrentEndpointTextView.setText("Endpoint/Audio Route NOT ESTABLISHED");
+ updateCallId();
+ mTelecomManager = getSystemService(TelecomManager.class);
+ mMediaPlayer = Utils.createMediaPlayer(getApplicationContext());
+ mAudioRecord = Utils.createAudioRecord();
+ mAudioRecord.registerAudioRecordingCallback(Runnable::run, mAudioRecordingCallback);
+
+ if (mVoipCall == null) {
+ addCall();
+ }
+
+ findViewById(R.id.set_call_active_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ updateCurrentEndpoint();
+ if (canUseCallControl()) {
+ mVoipCall.mCallControl.setActive(Runnable::run,
+ Utils.getLoggableOutcomeReceiver("setActive"));
+ }
+ mAudioRecord.startRecording();
+ mMediaPlayer.start();
+ }
+ });
+
+
+ findViewById(R.id.answer_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ updateCurrentEndpoint();
+ if (canUseCallControl() && mCallDirection != DIRECTION_OUTGOING) {
+ mVoipCall.mCallControl.answer(AUDIO_CALL, Runnable::run,
+ Utils.getLoggableOutcomeReceiver("answer"));
+ mAudioRecord.startRecording();
+ mMediaPlayer.start();
+ }
+ }
+ });
+
+
+ findViewById(R.id.set_call_inactive_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl()) {
+ mVoipCall.mCallControl.setInactive(Runnable::run,
+ Utils.getLoggableOutcomeReceiver("setInactive"));
+ }
+ mAudioRecord.stop();
+ mMediaPlayer.pause();
+ }
+ });
+
+ findViewById(R.id.disconnect_call_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ disconnectAndStopAudio();
+ finish();
+ }
+ });
+
+ findViewById(R.id.start_stream_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl()) {
+ mVoipCall.mCallControl.startCallStreaming(Runnable::run,
+ Utils.getLoggableOutcomeReceiver("startCallStream"));
+ }
+ }
+ });
+
+ findViewById(R.id.request_earpiece).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl() && mVoipCall.mEarpieceEndpoint != null) {
+ requestEndpointChange(mVoipCall.mEarpieceEndpoint,
+ "Request EARPIECE Endpoint:");
+ }
+ }
+ });
+
+ findViewById(R.id.request_speaker).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl() && mVoipCall.mSpeakerEndpoint != null) {
+ requestEndpointChange(mVoipCall.mSpeakerEndpoint,
+ "Request SPEAKER Endpoint:");
+ }
+ }
+ });
+
+ findViewById(R.id.request_bluetooth).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl() && mVoipCall.mBluetoothEndpoint != null) {
+ requestEndpointChange(mVoipCall.mBluetoothEndpoint,
+ "Request BLUETOOTH Endpoint:");
+ }
+ }
+ });
+
+ findViewById(R.id.crash_app).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // To test edge cases, it may be useful to crash the app. To do this, throwing a
+ // RuntimeException is sufficient.
+ throw new RuntimeException(
+ "Intentionally throwing RuntimeException from InCallActivity");
+ }
+ });
+
+ findViewById(R.id.updateCallStyleNotification).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Utils.updateCallStyleNotification_toOngoingCall(getApplicationContext());
+ }
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ Log.i(TAG, "onStop: InCallActivity has stopped");
+ super.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(TAG, "onDestroy: InCallActivity has been destroyed");
+ disconnectAndStopAudio();
+ super.onDestroy();
+ }
+
+ private boolean canUseCallControl() {
+ return mVoipCall != null && mVoipCall.mCallControl != null;
+ }
+
+ private void updateCurrentEndpoint() {
+ if (mCurrentEndpointTextView != null) {
+ if (mVoipCall != null && mVoipCall.mCurrentEndpoint != null) {
+ mCurrentEndpointTextView.setText("CallEndpoint=[" +
+ mVoipCall.mCurrentEndpoint.getEndpointName() + "]");
+ }
+ }
+ }
+
+ private void updateCurrentEndpointWithOnResult(CallEndpoint endpoint) {
+ if (mCurrentEndpointTextView != null) {
+ if (mVoipCall != null && mVoipCall.mCurrentEndpoint != null) {
+ mCurrentEndpointTextView.setText("CallEndpoint=[" +
+ endpoint.getEndpointName() + "]");
+ }
+ }
+ }
+
+ private void updateCallId() {
+ TextView view = findViewById(R.id.getCallIdTextView);
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ if (canUseCallControl()) {
+ String id = mVoipCall.mCallControl.getCallId().toString();
+ sb.append(id);
+ } else {
+ sb.append("Error Getting Id");
+ }
+ sb.append("]");
+ try {
+ view.setText(sb.toString());
+ }
+ catch (Exception e){
+ // ignore updating the ui
+ }
+ }
+
+ private void addCall() {
+ mVoipCall = new MyVoipCall("123");
+
+ CallAttributes callAttributes =
+ new CallAttributes.Builder(
+ Utils.PHONE_ACCOUNT_HANDLE,
+ mCallDirection,
+ "Alan Turing",
+ Uri.parse("tel:+16506959001")).build();
+
+ mTelecomManager.addCall(callAttributes, Runnable::run,
+ new OutcomeReceiver<CallControl, CallException>() {
+ @Override
+ public void onResult(CallControl callControl) {
+ Log.i(TAG, "addCall: onResult: callback fired");
+ Utils.postIncomingCallStyleNotification(getApplicationContext());
+ mVoipCall.onAddCallControl(callControl);
+ updateCallId();
+ updateCurrentEndpoint();
+ }
+
+ @Override
+ public void onError(CallException exception) {
+
+ }
+ },
+ mVoipCall, mVoipCall);
+ }
+
+ private void disconnectAndStopAudio() {
+ if (mVoipCall != null) {
+ mVoipCall.mCallControl.disconnect(
+ new DisconnectCause(DisconnectCause.LOCAL),
+ Runnable::run,
+ Utils.getLoggableOutcomeReceiver("disconnect"));
+ }
+ mMediaPlayer.stop();
+ mAudioRecord.stop();
+ try {
+ mAudioRecord.unregisterAudioRecordingCallback(mAudioRecordingCallback);
+ Utils.clearNotification(getApplicationContext());
+ } catch (Exception e) {
+ // pass through
+ }
+ }
+
+ private void requestEndpointChange(CallEndpoint endpoint, String tag) {
+ mVoipCall.mCallControl.requestCallEndpointChange(
+ endpoint,
+ Runnable::run,
+ new OutcomeReceiver<Void, CallException>() {
+ @Override
+ public void onResult(Void result) {
+ Log.i(TAG, String.format("requestEndpointChange: success w/ %s", tag));
+ updateCurrentEndpointWithOnResult(endpoint);
+ }
+
+ @Override
+ public void onError(CallException e) {
+ Log.i(TAG, String.format("requestEndpointChange: %s failed to switch to "
+ + "endpoint=[%s] due to exception=[%s]", tag, endpoint, e));
+ }
+ });
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
new file mode 100644
index 000000000..e2b5b1447
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
@@ -0,0 +1,123 @@
+/*
+ * 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.transactionalVoipApp;
+
+import android.os.Bundle;
+import android.telecom.CallControlCallback;
+import android.telecom.CallEndpoint;
+import android.telecom.CallControl;
+import android.telecom.CallEventCallback;
+import android.telecom.DisconnectCause;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+
+import java.util.function.Consumer;
+
+public class MyVoipCall implements CallControlCallback, CallEventCallback {
+
+ private static final String TAG = "MyVoipCall";
+ private final String mCallId;
+ public CallControl mCallControl;
+ public CallEndpoint mCurrentEndpoint;
+ public CallEndpoint mEarpieceEndpoint;
+ public CallEndpoint mSpeakerEndpoint;
+ public CallEndpoint mBluetoothEndpoint;
+ List<CallEndpoint> mAvailableEndpoint = new ArrayList<>();
+
+ MyVoipCall(String id) {
+ mCallId = id;
+ }
+
+ public void onAddCallControl(@NonNull CallControl callControl) {
+ mCallControl = callControl;
+ }
+
+ @Override
+ public void onSetActive(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onSetActive: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onSetInactive(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onSetInactive: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onAnswer(int videoState, @NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onAnswer: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onDisconnect(@NonNull DisconnectCause cause,
+ @NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onDisconnect: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onCallStreamingStarted(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onCallStreamingStarted: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onCallStreamingFailed(int reason) {
+ Log.i(TAG, String.format("onCallStreamingFailed: id=[%s], reason=[%d]", mCallId, reason));
+ }
+
+ @Override
+ public void onEvent(String event, Bundle extras) {
+ Log.i(TAG, String.format("onEvent: id=[%s], event=[%s], extras=[%s]",
+ mCallId, event, extras));
+ }
+
+ @Override
+ public void onCallEndpointChanged(@NonNull CallEndpoint newCallEndpoint) {
+ Log.i(TAG, String.format("onCallEndpointChanged: endpoint=[%s]", newCallEndpoint));
+ mCurrentEndpoint = newCallEndpoint;
+ }
+
+ @Override
+ public void onAvailableCallEndpointsChanged(
+ @NonNull List<CallEndpoint> availableEndpoints) {
+ Log.i(TAG, String.format("onAvailableCallEndpointsChanged: callId=[%s]", mCallId));
+ for (CallEndpoint endpoint : availableEndpoints) {
+ Log.i(TAG, String.format("endpoint=[%s]", endpoint));
+ if (endpoint != null && endpoint.getEndpointType() == CallEndpoint.TYPE_EARPIECE) {
+ mEarpieceEndpoint = endpoint;
+ }
+ if (endpoint != null && endpoint.getEndpointType() == CallEndpoint.TYPE_SPEAKER) {
+ mSpeakerEndpoint = endpoint;
+ }
+ if (endpoint != null && endpoint.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH) {
+ mBluetoothEndpoint = endpoint;
+ }
+ }
+ mAvailableEndpoint = availableEndpoints;
+ }
+
+ @Override
+ public void onMuteStateChanged(boolean isMuted) {
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
new file mode 100644
index 000000000..ec448b297
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
@@ -0,0 +1,176 @@
+/*
+ * 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.transactionalVoipApp;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioRecordingConfiguration;
+import android.media.MediaPlayer;
+import android.media.MediaRecorder;
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.util.Log;
+
+import java.util.List;
+
+public class Utils {
+ public static final String TAG = "TransactionalAppUtils";
+ public static final String CALLER_NAME = "Sundar Pichai";
+ public static final String sEXTRAS_KEY = "ExtrasKey";
+ public static final String sCALL_DIRECTION_KEY = "CallDirectionKey";
+ public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
+ public static final int CALL_NOTIFICATION_ID = 123456;
+ private static final int SAMPLING_RATE_HZ = 44100;
+
+ public static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE = new PhoneAccountHandle(
+ new ComponentName("com.android.server.telecom.transactionalVoipApp",
+ "com.android.server.telecom.transactionalVoipApp.VoipAppMainActivity"), "123");
+
+ public static final PhoneAccount PHONE_ACCOUNT =
+ PhoneAccount.builder(PHONE_ACCOUNT_HANDLE, "test label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+
+
+ public static Notification createCallStyleNotification(Context context) {
+ Intent answerIntent = new Intent(context, InCallActivity.class);
+ Intent rejectIntent = new Intent(context, InCallActivity.class);
+
+ // Creating a pending intent and wrapping our intent
+ PendingIntent pendingAnswer = PendingIntent.getActivity(context, 0,
+ answerIntent, PendingIntent.FLAG_IMMUTABLE);
+ PendingIntent pendingReject = PendingIntent.getActivity(context, 0,
+ rejectIntent, PendingIntent.FLAG_IMMUTABLE);
+
+
+ Notification callStyleNotification = new Notification.Builder(context,
+ CHANNEL_ID)
+ .setContentText("Answer/Reject call")
+ .setContentTitle("Incoming call")
+ .setSmallIcon(R.drawable.ic_android_black_24dp)
+ .setStyle(Notification.CallStyle.forIncomingCall(
+ new Person.Builder().setName(CALLER_NAME).setImportant(true).build(),
+ pendingAnswer, pendingReject)
+ )
+ .setFullScreenIntent(pendingAnswer, true)
+ .setOngoing(true)
+ .build();
+
+ return callStyleNotification;
+ }
+
+ public static void postIncomingCallStyleNotification(Context context) {
+ NotificationManager nm = context.getSystemService(NotificationManager.class);
+ nm.notify(Utils.CALL_NOTIFICATION_ID, createCallStyleNotification(context));
+ }
+
+ public static void updateCallStyleNotification_toOngoingCall(Context context) {
+ PendingIntent ongoingCall = PendingIntent.getActivity(context, 0,
+ new Intent(""), PendingIntent.FLAG_IMMUTABLE);
+
+ Notification callStyleNotification = new Notification.Builder(context,
+ CHANNEL_ID)
+ .setContentText("active call in the TransactionalTestApp")
+ .setContentTitle("Ongoing call")
+ .setSmallIcon(R.drawable.ic_android_black_24dp)
+ .setStyle(Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName(CALLER_NAME).setImportant(true).build(),
+ ongoingCall)
+ )
+ .setFullScreenIntent(ongoingCall, true)
+ .setOngoing(true)
+ .build();
+
+ NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+
+ notificationManager.notify(CALL_NOTIFICATION_ID, callStyleNotification);
+ }
+
+ public static void clearNotification(Context context) {
+ NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.cancel(CALL_NOTIFICATION_ID);
+ }
+ }
+
+ public static MediaPlayer createMediaPlayer(Context context) {
+ int audioToPlay = (Math.random() > 0.5f) ?
+ com.android.server.telecom.transactionalVoipApp.R.raw.sample_audio :
+ com.android.server.telecom.transactionalVoipApp.R.raw.sample_audio2;
+ MediaPlayer mediaPlayer = MediaPlayer.create(context, audioToPlay);
+ mediaPlayer.setLooping(true);
+ return mediaPlayer;
+ }
+
+ public static AudioRecord createAudioRecord() {
+ return new AudioRecord.Builder()
+ .setAudioFormat(new AudioFormat.Builder()
+ .setSampleRate(SAMPLING_RATE_HZ)
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
+ .setAudioSource(MediaRecorder.AudioSource.DEFAULT)
+ .setBufferSizeInBytes(
+ AudioRecord.getMinBufferSize(SAMPLING_RATE_HZ,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT) * 10)
+ .build();
+ }
+
+
+ public static AudioManager.AudioRecordingCallback getAudioRecordingCallback() {
+ return new AudioManager.AudioRecordingCallback() {
+ @Override
+ public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
+ super.onRecordingConfigChanged(configs);
+
+ for (AudioRecordingConfiguration config : configs) {
+ if (config != null) {
+ Log.i(TAG, String.format("onRecordingConfigChanged: random: "
+ + "isClientSilenced=[%b], config=[%s]",
+ config.isClientSilenced(), config));
+ }
+ }
+ }
+ };
+ }
+
+ public static OutcomeReceiver<Void, CallException> getLoggableOutcomeReceiver(String tag) {
+ return new OutcomeReceiver<Void, CallException>() {
+ @Override
+ public void onResult(Void result) {
+ Log.i(TAG, tag + " : onResult");
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, tag + " : onError");
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
new file mode 100644
index 000000000..72a3906e2
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
@@ -0,0 +1,146 @@
+/*
+ * 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.transactionalVoipApp;
+
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
+
+import android.app.Activity;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.ToggleButton;
+
+public class VoipAppMainActivity extends Activity {
+ private static final String TAG = "VoipAppMainActivity";
+ private static final String ACT_STATE_TAG = "VoipActivityState";
+ private static TelecomManager mTelecomManager;
+ NotificationManager mNotificationManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, ACT_STATE_TAG + "onCreate");
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+
+ mTelecomManager = getSystemService(TelecomManager.class);
+ mNotificationManager = getSystemService(NotificationManager.class);
+ // create a notification channel
+ if (mNotificationManager != null) {
+ mNotificationManager.createNotificationChannel(new NotificationChannel(
+ Utils.CHANNEL_ID, "new call channel",
+ NotificationManager.IMPORTANCE_DEFAULT));
+ }
+
+ // register account
+ findViewById(R.id.registerButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTelecomManager.registerPhoneAccount(Utils.PHONE_ACCOUNT);
+ }
+ });
+
+ // Start a foreground service that will post a notification within 10 seconds.
+ // This is helpful for debugging scenarios where the app is in the background and posting
+ // an incoming call notification.
+ findViewById(R.id.startForegroundService).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent startForegroundService = new Intent(getApplicationContext(),
+ BackgroundIncomingCallService.class);
+ getApplicationContext().startForegroundService(startForegroundService);
+ }
+ });
+
+
+ // post a new call notification and start an InCall activity
+ findViewById(R.id.startOutgoingCall).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startInCallActivity(DIRECTION_OUTGOING);
+ }
+ });
+
+ // post a new call notification and start an InCall activity
+ findViewById(R.id.startIncomingCall).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startInCallActivity(DIRECTION_INCOMING);
+ }
+ });
+
+ }
+
+ private void startInCallActivity(int direction) {
+ Bundle extras = new Bundle();
+ extras.putInt(Utils.sCALL_DIRECTION_KEY, direction);
+ Intent intent = new Intent(getApplicationContext(), InCallActivity.class);
+ intent.putExtra(Utils.sEXTRAS_KEY, extras);
+ startActivity(intent);
+ }
+
+ @Override
+ protected void onResume() {
+ Log.i(TAG, ACT_STATE_TAG + " onResume: When the activity enters the Resumed state,"
+ + " it comes to the foreground");
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ Log.i(TAG, ACT_STATE_TAG + " onPause: The system calls this method as the first"
+ + " indication that the user is leaving your activity. It indicates that the"
+ + " activity is no longer in the foreground, but it is still visible if the user"
+ + " is in multi-window mode");
+ super.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ Log.i(TAG, ACT_STATE_TAG + "onStop: When your activity is no longer visible to"
+ + " the user, it enters the Stopped state,");
+ super.onStop();
+ }
+
+ @Override
+ protected void onRestart() {
+ Log.i(TAG, ACT_STATE_TAG + " onRestart: onStop has called onRestart and the "
+ + "activity comes back to interact with the user");
+ super.onRestart();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(TAG, ACT_STATE_TAG + " onDestroy: is called before the activity is"
+ + " destroyed. ");
+ Utils.clearNotification(getApplicationContext());
+ super.onDestroy();
+ }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index e8c69d419..4ca6030e9 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -21,13 +21,14 @@
<uses-sdk
android:minSdkVersion="23"
- android:targetSdkVersion="23" />
+ android:targetSdkVersion="33" />
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
<!-- TODO: Needed because we call BluetoothAdapter.getDefaultAdapter() statically, and
BluetoothAdapter is a final class. -->
<uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
-
<!-- TODO: Needed because we call ActivityManager.getCurrentUser() statically. -->
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
@@ -43,6 +44,9 @@
<!-- 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" />
<application android:label="@string/app_name"
android:debuggable="true">
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 049a50163..bd81a2ffe 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -16,8 +16,11 @@
package com.android.server.telecom.tests;
+import static com.android.server.telecom.tests.ConnectionServiceFixture.STATUS_HINTS_EXTRA;
+
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.nullable;
@@ -38,12 +41,17 @@ import android.content.Context;
import android.content.IContentProvider;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
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;
@@ -54,12 +62,14 @@ import android.telecom.Log;
import android.telecom.ParcelableCall;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
+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.SmallTest;
import com.android.internal.telecom.IInCallAdapter;
import android.telecom.CallerInfo;
@@ -87,6 +97,7 @@ import java.util.concurrent.TimeUnit;
*/
@RunWith(JUnit4.class)
public class BasicCallTests extends TelecomSystemTest {
+ private static final String CALLING_PACKAGE = BasicCallTests.class.getPackageName();
private static final String TEST_BUNDLE_KEY = "android.telecom.extra.TEST";
private static final String TEST_EVENT = "android.telecom.event.TEST";
@@ -194,7 +205,7 @@ public class BasicCallTests extends TelecomSystemTest {
@Test
public void testTelecomManagerAcceptRingingVideoCall() throws Exception {
IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA, null);
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
@@ -223,7 +234,7 @@ public class BasicCallTests extends TelecomSystemTest {
@Test
public void testTelecomManagerAcceptRingingVideoCallAsAudio() throws Exception {
IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA, null);
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
@@ -254,7 +265,7 @@ public class BasicCallTests extends TelecomSystemTest {
@Test
public void testTelecomManagerAcceptRingingInvalidVideoState() throws Exception {
IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA, null);
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
@@ -327,7 +338,7 @@ public class BasicCallTests extends TelecomSystemTest {
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null));
mTelecomSystem.getTelecomServiceImpl().getBinder()
- .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras);
+ .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras, CALLING_PACKAGE);
waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
TEST_TIMEOUT);
@@ -392,7 +403,7 @@ public class BasicCallTests extends TelecomSystemTest {
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
Uri.fromParts(PhoneAccount.SCHEME_TEL, "650-555-1212", null));
mTelecomSystem.getTelecomServiceImpl().getBinder()
- .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras);
+ .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras, CALLING_PACKAGE);
waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
TEST_TIMEOUT);
@@ -442,7 +453,7 @@ public class BasicCallTests extends TelecomSystemTest {
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
Uri.fromParts(PhoneAccount.SCHEME_TEL, "650-555-1212", null));
mTelecomSystem.getTelecomServiceImpl().getBinder()
- .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras);
+ .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras, CALLING_PACKAGE);
waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
TEST_TIMEOUT);
@@ -494,7 +505,7 @@ public class BasicCallTests extends TelecomSystemTest {
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null));
mTelecomSystem.getTelecomServiceImpl().getBinder()
- .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras);
+ .addNewIncomingCall(mPhoneAccountA0.getAccountHandle(), extras, CALLING_PACKAGE);
waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
TEST_TIMEOUT);
@@ -692,13 +703,13 @@ public class BasicCallTests extends TelecomSystemTest {
@MediumTest
@Test
public void testBasicConferenceCall() throws Exception {
- makeConferenceCall();
+ makeConferenceCall(null, null);
}
@MediumTest
@Test
public void testAddCallToConference1() throws Exception {
- ParcelableCall conferenceCall = makeConferenceCall();
+ ParcelableCall conferenceCall = makeConferenceCall(null, null);
IdPair callId3 = startAndMakeActiveOutgoingCall("650-555-1214",
mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
// testAddCallToConference{1,2} differ in the order of arguments to InCallAdapter#conference
@@ -716,7 +727,7 @@ public class BasicCallTests extends TelecomSystemTest {
@MediumTest
@Test
public void testAddCallToConference2() throws Exception {
- ParcelableCall conferenceCall = makeConferenceCall();
+ ParcelableCall conferenceCall = makeConferenceCall(null, null);
IdPair callId3 = startAndMakeActiveOutgoingCall("650-555-1214",
mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
mInCallServiceFixtureX.getInCallAdapter()
@@ -974,7 +985,7 @@ public class BasicCallTests extends TelecomSystemTest {
public void testOutgoingCallSelectPhoneAccountVideo() throws Exception {
startOutgoingPhoneCallPendingCreateConnection("650-555-1212",
null, mConnectionServiceFixtureA,
- Process.myUserHandle(), VideoProfile.STATE_BIDIRECTIONAL);
+ Process.myUserHandle(), VideoProfile.STATE_BIDIRECTIONAL, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
assert(call.isVideoCallingSupportedByPhoneAccount());
@@ -997,7 +1008,7 @@ public class BasicCallTests extends TelecomSystemTest {
public void testOutgoingCallSelectPhoneAccountNoVideo() throws Exception {
startOutgoingPhoneCallPendingCreateConnection("650-555-1212",
null, mConnectionServiceFixtureA,
- Process.myUserHandle(), VideoProfile.STATE_BIDIRECTIONAL);
+ Process.myUserHandle(), VideoProfile.STATE_BIDIRECTIONAL, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
assert(call.isVideoCallingSupportedByPhoneAccount());
@@ -1212,4 +1223,145 @@ public class BasicCallTests extends TelecomSystemTest {
assertTrue(muteValues.get(0));
assertFalse(muteValues.get(1));
}
+
+ /**
+ * Verifies that StatusHints image is validated in ConnectionServiceWrapper#addConferenceCall
+ * when the image doesn't belong to the calling user. Simulates a scenario where an app
+ * could manipulate the contents of the bundle and send it via the binder to upload an image
+ * from another user.
+ *
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testValidateStatusHintsImage_addConferenceCall() throws Exception {
+ Intent callIntent1 = new Intent();
+ // Stub intent for call2
+ Intent callIntent2 = new Intent();
+ Bundle callExtras1 = new Bundle();
+ Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+ // Load StatusHints extra into TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS to be processed
+ // as the call extras. This will be leveraged in ConnectionServiceFixture to set the
+ // StatusHints for the given connection.
+ StatusHints statusHints = new StatusHints(icon);
+ assertNotNull(statusHints.getIcon());
+ callExtras1.putParcelable(STATUS_HINTS_EXTRA, statusHints);
+ callIntent1.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras1);
+
+ // Start conference call to invoke ConnectionServiceWrapper#addConferenceCall.
+ // Note that the calling user would be User 0.
+ ParcelableCall conferenceCall = makeConferenceCall(callIntent1, callIntent2);
+
+ // Ensure that StatusHints was set.
+ assertNotNull(mInCallServiceFixtureX.getCall(mInCallServiceFixtureX.mLatestCallId)
+ .getStatusHints());
+ // Ensure that the StatusHints image icon was disregarded.
+ assertNull(mInCallServiceFixtureX.getCall(mInCallServiceFixtureX.mLatestCallId)
+ .getStatusHints().getIcon());
+ }
+
+ /**
+ * Verifies that StatusHints image is validated in
+ * ConnectionServiceWrapper#handleCreateConnectionComplete when the image doesn't belong to the
+ * calling user. Simulates a scenario where an app could manipulate the contents of the
+ * bundle and send it via the binder to upload an image from another user.
+ *
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testValidateStatusHintsImage_handleCreateConnectionComplete() throws Exception {
+ Bundle extras = new Bundle();
+ Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+ // Load the bundle with the test extra in order to simulate an app directly invoking the
+ // binder on ConnectionServiceWrapper#handleCreateConnectionComplete.
+ StatusHints statusHints = new StatusHints(icon);
+ assertNotNull(statusHints.getIcon());
+ extras.putParcelable(STATUS_HINTS_EXTRA, statusHints);
+
+ // Start incoming call with StatusHints extras
+ // Note that the calling user in ConnectionServiceWrapper#handleCreateConnectionComplete
+ // would be User 0.
+ IdPair ids = startIncomingPhoneCallWithExtras("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA, extras);
+
+ // Ensure that StatusHints was set.
+ assertNotNull(mInCallServiceFixtureX.getCall(ids.mCallId).getStatusHints());
+ // Ensure that the StatusHints image icon was disregarded.
+ assertNull(mInCallServiceFixtureX.getCall(ids.mCallId).getStatusHints().getIcon());
+ }
+
+ /**
+ * Verifies that StatusHints image is validated in ConnectionServiceWrapper#setStatusHints
+ * when the image doesn't belong to the calling user. Simulates a scenario where an app
+ * could manipulate the contents of the bundle and send it via the binder to upload an image
+ * from another user.
+ *
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testValidateStatusHintsImage_setStatusHints() throws Exception {
+ IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1214",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ // Modify existing connection with StatusHints image exploit
+ Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+ StatusHints statusHints = new StatusHints(icon);
+ assertNotNull(statusHints.getIcon());
+ ConnectionServiceFixture.ConnectionInfo connectionInfo = mConnectionServiceFixtureA
+ .mConnectionById.get(outgoing.mConnectionId);
+ connectionInfo.statusHints = statusHints;
+
+ // Invoke ConnectionServiceWrapper#setStatusHints.
+ // Note that the calling user would be User 0.
+ mConnectionServiceFixtureA.sendSetStatusHints(outgoing.mConnectionId);
+ waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+ TEST_TIMEOUT);
+
+ // Ensure that StatusHints was set.
+ assertNotNull(mInCallServiceFixtureX.getCall(outgoing.mCallId).getStatusHints());
+ // Ensure that the StatusHints image icon was disregarded.
+ assertNull(mInCallServiceFixtureX.getCall(outgoing.mCallId)
+ .getStatusHints().getIcon());
+ }
+
+ /**
+ * Verifies that StatusHints image is validated in
+ * ConnectionServiceWrapper#addExistingConnection when the image doesn't belong to the calling
+ * user. Simulates a scenario where an app could manipulate the contents of the bundle and
+ * send it via the binder to upload an image from another user.
+ *
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ 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/");
+ StatusHints modifiedStatusHints = new StatusHints(icon);
+ assertNotNull(modifiedStatusHints.getIcon());
+ ConnectionServiceFixture.ConnectionInfo connectionInfo = mConnectionServiceFixtureA
+ .mConnectionById.get(outgoing.mConnectionId);
+ connectionInfo.statusHints = modifiedStatusHints;
+
+ // Invoke ConnectionServiceWrapper#addExistingConnection.
+ // Note that the calling user would be User 0.
+ mConnectionServiceFixtureA.sendAddExistingConnection(outgoing.mConnectionId);
+ waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+ TEST_TIMEOUT);
+
+ // Ensure that StatusHints was set. Due to test setup, the ParcelableConnection object that
+ // is passed into sendAddExistingConnection is instantiated on invocation. The call's
+ // StatusHints are not updated at the time of completion, so instead, we can verify that
+ // the ParcelableConnection object was modified.
+ assertNotNull(mConnectionServiceFixtureA.mLatestParcelableConnection.getStatusHints());
+ // Ensure that the StatusHints image icon was disregarded.
+ assertNull(mConnectionServiceFixtureA.mLatestParcelableConnection
+ .getStatusHints().getIcon());
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
new file mode 100644
index 000000000..7e197fe65
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
@@ -0,0 +1,867 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+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.content.ComponentName;
+import android.net.Uri;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAnomalyWatchdog;
+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.EmergencyCallDiagnosticLogger;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.ui.ToastFactory;
+
+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.Mock;
+
+@RunWith(JUnit4.class)
+public class CallAnomalyWatchdogTest extends TelecomTestCase {
+ private static final ComponentName COMPONENT_NAME_1 = ComponentName
+ .unflattenFromString("com.foo/.Blah");
+ private static final PhoneAccountHandle SIM_1_HANDLE = new PhoneAccountHandle(
+ COMPONENT_NAME_1, "Sim1");
+ private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.
+ Builder(SIM_1_HANDLE, "Sim1")
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setIsEnabled(true)
+ .build();
+
+ private final static long TEST_VOIP_TRANSITORY_MILLIS = 100L;
+ private final static long TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS = 150L;
+ private final static long TEST_NON_VOIP_TRANSITORY_MILLIS = 200L;
+ private final static long TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS = 250L;
+ private final static long TEST_VOIP_INTERMEDIATE_MILLIS = 300L;
+ private final static long TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS = 350L;
+ private final static long TEST_NON_VOIP_INTERMEDIATE_MILLIS = 400L;
+ private final static long TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS = 450L;
+
+ private CallAnomalyWatchdog mCallAnomalyWatchdog;
+ private TestScheduledExecutorService mTestScheduledExecutorService;
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
+ @Mock private Timeouts.Adapter mTimeouts;
+ @Mock private CallsManager mMockCallsManager;
+ @Mock private CallerInfoLookupHelper mMockCallerInfoLookupHelper;
+ @Mock private PhoneAccountRegistrar mMockPhoneAccountRegistrar;
+ @Mock private ClockProxy mMockClockProxy;
+ @Mock private ToastFactory mMockToastProxy;
+ @Mock private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
+ @Mock private ConnectionServiceWrapper mMockConnectionService;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
+
+ @Mock private EmergencyCallDiagnosticLogger mMockEmergencyCallDiagnosticLogger;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mMockCallerInfoLookupHelper).when(mMockCallsManager).getCallerInfoLookupHelper();
+ doReturn(mMockPhoneAccountRegistrar).when(mMockCallsManager).getPhoneAccountRegistrar();
+ doReturn(SIM_1_ACCOUNT).when(mMockPhoneAccountRegistrar).getPhoneAccountUnchecked(
+ eq(SIM_1_HANDLE));
+ mTestScheduledExecutorService = new TestScheduledExecutorService();
+
+ when(mTimeouts.getVoipCallTransitoryStateTimeoutMillis()).
+ thenReturn(TEST_VOIP_TRANSITORY_MILLIS);
+ when(mTimeouts.getVoipEmergencyCallTransitoryStateTimeoutMillis()).
+ thenReturn(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS);
+ when(mTimeouts.getNonVoipCallTransitoryStateTimeoutMillis()).
+ thenReturn(TEST_NON_VOIP_TRANSITORY_MILLIS);
+ when(mTimeouts.getNonVoipEmergencyCallTransitoryStateTimeoutMillis()).
+ thenReturn(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS);
+ when(mTimeouts.getVoipCallIntermediateStateTimeoutMillis()).
+ thenReturn(TEST_VOIP_INTERMEDIATE_MILLIS);
+ when(mTimeouts.getVoipEmergencyCallIntermediateStateTimeoutMillis()).
+ thenReturn(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS);
+ when(mTimeouts.getNonVoipCallIntermediateStateTimeoutMillis()).
+ thenReturn(TEST_NON_VOIP_INTERMEDIATE_MILLIS);
+ when(mTimeouts.getNonVoipEmergencyCallIntermediateStateTimeoutMillis()).
+ thenReturn(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS);
+
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(0L);
+ doReturn(new ComponentName(mContext, CallTest.class))
+ .when(mMockConnectionService).getComponentName();
+ mCallAnomalyWatchdog = new CallAnomalyWatchdog(mTestScheduledExecutorService, mLock,
+ mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger);
+ mCallAnomalyWatchdog.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Helper function that setups the call being tested.
+ */
+ private Call setupCallHelper(int callState, boolean isCreateConnectionComplete,
+ ConnectionServiceWrapper service, boolean isVoipAudioMode, boolean isEmergencyCall) {
+ Call call = getCall();
+ call.setState(callState, "foo");
+ call.setIsCreateConnectionComplete(isCreateConnectionComplete);
+ if (service != null) call.setConnectionService(service);
+ call.setIsVoipAudioMode(isVoipAudioMode);
+ call.setIsEmergencyCall(isEmergencyCall);
+ mCallAnomalyWatchdog.onCallAdded(call);
+ return call;
+ }
+
+ /**
+ * Test that the anomaly call state class correctly reports whether the state is transitory or
+ * not for the purposes of the call anomaly watchdog.
+ */
+ @Test
+ public void testAnomalyCallStateIsTransitory() {
+ // When connection creation isn't complete, most states are transitory from the anomaly
+ // tracker's point of view.
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.NEW,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.CONNECTING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.DIALING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.RINGING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.ACTIVE,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.ON_HOLD,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTED,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.ABORTED,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.PULLING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.ANSWERED,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.AUDIO_PROCESSING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.SIMULATED_RINGING,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ // When create connection is complete, these few are considered to be transitory.
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.NEW,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.CONNECTING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.ANSWERED,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+
+ // These are never considered transitory
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SELECT_PHONE_ACCOUNT,
+ false /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SELECT_PHONE_ACCOUNT,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DIALING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.RINGING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ACTIVE,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ON_HOLD,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTED,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ABORTED,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.PULLING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.AUDIO_PROCESSING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SIMULATED_RINGING,
+ true /* isCreateConnectionComplete */, 0L).isInTransitoryState());
+ }
+
+ /**
+ * Test that the anomaly call state class correctly reports whether the state is intermediate or
+ * not for the purposes of the call anomaly watchdog.
+ */
+ @Test
+ public void testAnomalyCallStateIsIntermediate() {
+ // When connection creation isn't complete, most states are not intermediate from
+ // the anomaly tracker's point of view.
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.NEW,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.CONNECTING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SELECT_PHONE_ACCOUNT,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DIALING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.RINGING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ACTIVE,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ON_HOLD,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTED,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ABORTED,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.PULLING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ANSWERED,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.AUDIO_PROCESSING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SIMULATED_RINGING,
+ false /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+
+ // If it is not in DIALING and RINGING state, it is not considered as an intermediate state.
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.NEW,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.CONNECTING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SELECT_PHONE_ACCOUNT,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ACTIVE,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ON_HOLD,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTED,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ABORTED,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.DISCONNECTING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.PULLING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.ANSWERED,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.AUDIO_PROCESSING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertFalse(new CallAnomalyWatchdog.WatchdogCallState(CallState.SIMULATED_RINGING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+
+ // These are considered as an intermediate state.
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.DIALING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ assertTrue(new CallAnomalyWatchdog.WatchdogCallState(CallState.RINGING,
+ true /* isCreateConnectionComplete */, 0L).isInIntermediateState());
+ }
+
+ /**
+ * Emulate the case where a new incoming VoIP call is added to the watchdog.
+ * CallsManager creates calls in a ringing state before they're even created by the underlying
+ * ConnectionService. The call is added by the connection service before the timeout expires,
+ * so we verify that the call does not get disconnected.
+ */
+ @Test
+ public void testAddVoipRingingCall() {
+ Call call = setupCallHelper(CallState.RINGING, false, null, true, false);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Gets added to connection service; this moves it to an intermediate state,
+ // so timeouts should be scheduled at this point.
+ call.setIsCreateConnectionComplete(true);
+ call.setConnectionService(mMockConnectionService);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock forward; we'll confirm that no timeout took place.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_INTERMEDIATE_MILLIS + 1);
+ // Should still be ringing.
+ assertEquals(CallState.RINGING, call.getState());
+ }
+
+ /**
+ * Emulate the case where a new incoming VoIP emergency call is added to the watchdog.
+ * CallsManager creates calls in a ringing state before they're even created by the underlying
+ * ConnectionService. The call is added by the connection service before the timeout expires,
+ * so we verify that the call does not get disconnected.
+ */
+ @Test
+ public void testAddVoipEmergencyRingingCall() {
+ Call call = setupCallHelper(CallState.RINGING, false, null, true, true);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Gets added to connection service; this moves it to an intermediate state,
+ // so timeouts should be scheduled at this point.
+ call.setIsCreateConnectionComplete(true);
+ call.setConnectionService(mMockConnectionService);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock forward; we'll confirm that no timeout took place.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+ // Should still be ringing.
+ assertEquals(CallState.RINGING, call.getState());
+ }
+
+ /**
+ * Emulate the case where a new incoming non-VoIP call is added to the watchdog.
+ * CallsManager creates calls in a ringing state before they're even created by the underlying
+ * ConnectionService. The call is added by the connection service before the timeout expires,
+ * so we verify that the call does not get disconnected.
+ */
+ @Test
+ public void testAddNonVoipRingingCall() {
+ Call call = setupCallHelper(CallState.RINGING, false, null, false, false);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Gets added to connection service; this moves it to an intermediate state,
+ // so timeouts should be scheduled at this point.
+ call.setIsCreateConnectionComplete(true);
+ call.setConnectionService(mMockConnectionService);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock forward; we'll confirm that no timeout took place.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+ // Should still be ringing.
+ assertEquals(CallState.RINGING, call.getState());
+ }
+
+ /**
+ * Emulate the case where a new incoming non-VoIP emergency call is added to the watchdog.
+ * CallsManager creates calls in a ringing state before they're even created by the underlying
+ * ConnectionService. The call is added by the connection service before the timeout expires,
+ * so we verify that the call does not get disconnected.
+ */
+ @Test
+ public void testAddNonVoipEmergencyRingingCall() {
+ Call call = setupCallHelper(CallState.RINGING, false, null, false, true);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Gets added to connection service; this moves it to an intermediate state,
+ // so timeouts should be scheduled at this point.
+ call.setIsCreateConnectionComplete(true);
+ call.setConnectionService(mMockConnectionService);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock forward; we'll confirm that no timeout took place.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+ // Should still be ringing.
+ assertEquals(CallState.RINGING, call.getState());
+ }
+
+ /**
+ * Emulate the case where a new incoming VoIP call is added to the watchdog.
+ * In this case, the ConnectionService doesn't respond promptly and the timeout will fire.
+ */
+ @Test
+ public void testAddVoipRingingCallTimeoutWithoutConnection() {
+ setupCallHelper(CallState.RINGING, false, null, true, false);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_VOIP_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_TRANSITORY_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming VoIP emergency call is added to the watchdog.
+ * In this case, the ConnectionService doesn't respond promptly and the timeout will fire.
+ */
+ @Test
+ public void testAddVoipEmergencyRingingCallTimeoutWithoutConnection() {
+ setupCallHelper(CallState.RINGING, false, null, true, true);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS +
+ 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming non-VoIP call is added to the watchdog.
+ * In this case, the ConnectionService doesn't respond promptly and the timeout will fire.
+ */
+ @Test
+ public void testAddNonVoipRingingCallTimeoutWithoutConnection() {
+ setupCallHelper(CallState.RINGING, false, null, false, false);;
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_NON_VOIP_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_TRANSITORY_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming non-VoIP emergency call is added to the watchdog.
+ * In this case, the ConnectionService doesn't respond promptly and the timeout will fire.
+ */
+ @Test
+ public void testAddNonVoipEmergencyRingingCallTimeoutWithoutConnection() {
+ setupCallHelper(CallState.RINGING, false, null, false, true);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming VoIP call is added to the watchdog.
+ * In this case, the timeout will fire in intermediate state.
+ */
+ @Test
+ public void testAddVoipRingingCallTimeoutWithConnection() {
+ setupCallHelper(CallState.RINGING, true, mMockConnectionService, true, false);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_INTERMEDIATE_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming VoIP emergency call is added to the watchdog.
+ * In this case, the timeout will fire in intermediate state.
+ */
+ @Test
+ public void testAddVoipEmergencyRingingCallTimeoutWithConnection() {
+ setupCallHelper(CallState.RINGING, true, mMockConnectionService, true, true);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming non-VoIP call is added to the watchdog.
+ * In this case, the timeout will fire in intermediate state.
+ */
+ @Test
+ public void testAddNonVoipRingingCallTimeoutWithConnection() {
+ setupCallHelper(CallState.RINGING, true, mMockConnectionService, false, false);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new incoming non-VoIP emergency call is added to the watchdog.
+ * In this case, the timeout will fire in intermediate state.
+ */
+ @Test
+ public void testAddNonVoipEmergencyRingingCallTimeoutWithConnection() {
+ setupCallHelper(CallState.RINGING, true, mMockConnectionService, false, true);
+
+ // Newly created call which hasn't been added; should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_EMERGENCY_INTERMEDIATE_MILLIS + 1);
+
+ // No timeouts should be pending at this point since the timeout fired.
+ assertEquals(0, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertEquals(0, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+ }
+
+ /**
+ * Emulate the case where a new outgoing VoIP call is added to the watchdog.
+ * In this case, the timeout will fire in transitory state.
+ */
+ @Test
+ public void testVoipPlaceCallTimeout() {
+ // Call will start in connecting state
+ Call call = setupCallHelper(CallState.CONNECTING, false, null, true, false);
+
+ // Assume it is created but the app never sets it to a proper state
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ // Its transitory, so should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_VOIP_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_TRANSITORY_MILLIS + 1);
+ }
+
+ /**
+ * Emulate the case where a new outgoing VoIP call is added to the watchdog.
+ * In this case, the timeout will fire in transitory state and should report an anomaly.
+ */
+ @Test
+ public void testVoipPlaceCallTimeoutReportAnomaly() {
+ // Call will start in connecting state
+ Call call = setupCallHelper(CallState.CONNECTING, false, null, true, false);
+
+ // Assume it is created but the app never sets it to a proper state
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_VOIP_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_TRANSITORY_MILLIS + 1);
+
+ //Ensure an anomaly was reported
+ verify(mAnomalyReporterAdapter).reportAnomaly(
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_UUID,
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_MSG);
+ }
+
+ /**
+ * Emulate the case where a new outgoing VoIP emergency call is added to the watchdog.
+ * In this case, the timeout will fire in transitory state and should report an emergency
+ * anomaly.
+ */
+ @Test
+ public void testVoipEmergencyPlaceCallTimeoutReportAnomaly() {
+ // Call will start in connecting state
+ Call call = setupCallHelper(CallState.CONNECTING, false, null, true, true);
+
+ // Assume it is created but the app never sets it to a proper state
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+
+ //Ensure an anomaly was reported
+ verify(mAnomalyReporterAdapter).reportAnomaly(
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_UUID,
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_EMERGENCY_CALL_MSG);
+ }
+
+ /**
+ * Emulate the case where a new outgoing VoIP emergency call is added to the watchdog.
+ * In this case, the timeout will fire in transitory state.
+ */
+ @Test
+ public void testVoipEmergencyPlaceCallTimeout() {
+ // Call will start in connecting state
+ Call call = setupCallHelper(CallState.CONNECTING, false, null, true, true);
+
+ // Assume it is created but the app never sets it to a proper state
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ // Its transitory, so should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+ }
+
+ /**
+ * Emulate the case where a new outgoing non-VoIP call is added to the watchdog.
+ * In this case, the timeout will fire in transitory state.
+ */
+ @Test
+ public void testNonVoipPlaceCallTimeout() {
+ // Call will start in connecting state
+ Call call = setupCallHelper(CallState.CONNECTING, false, null, false, false);
+
+ // Assume it is created but the app never sets it to a proper state
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ // Its transitory, so should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).thenReturn(TEST_NON_VOIP_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_TRANSITORY_MILLIS + 1);
+ }
+
+ /**
+ * Emulate the case where a new outgoing non-VoIP emergency call is added to the watchdog.
+ * In this case, the timeout will fire in transitory state.
+ */
+ @Test
+ public void testNonVoipEmergencyPlaceCallTimeout() {
+ // Call will start in connecting state
+ Call call = setupCallHelper(CallState.CONNECTING, false, null, false, true);
+
+ // Assume it is created but the app never sets it to a proper state
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onCallAdded(call);
+
+ // Its transitory, so should schedule timeout.
+ assertEquals(1, mTestScheduledExecutorService.getNumberOfScheduledRunnables());
+ assertTrue(mTestScheduledExecutorService.
+ isRunnableScheduledAtTime(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS));
+ assertEquals(1, mCallAnomalyWatchdog.getNumberOfScheduledTimeouts());
+
+ // Move the clock to fire the timeout.
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+ mTestScheduledExecutorService.
+ advanceTime(TEST_NON_VOIP_EMERGENCY_TRANSITORY_MILLIS + 1);
+ }
+
+ /**
+ * Emulate the case where a new incoming call is created but the connection fails for a known
+ * reason before being added to CallsManager. In this case, the watchdog should stop tracking
+ * the call and not trigger an anomaly report.
+ */
+ @Test
+ public void testIncomingCallCreatedButNotAddedNoAnomalyReport() {
+ //The call is created:
+ Call call = getCall();
+ call.setState(CallState.NEW, "foo");
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onStartCreateConnection(call);
+
+ //The connection fails before being added to CallsManager for a known reason:
+ call.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
+
+ // Move the clock forward:
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+
+ //Ensure an anomaly report is not generated:
+ verify(mAnomalyReporterAdapter, never()).reportAnomaly(
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_UUID,
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_MSG);
+ }
+
+ /**
+ * Emulate the case where a new outgoing call is created but the connection fails for a known
+ * reason before being added to CallsManager. In this case, the watchdog should stop tracking
+ * the call and not trigger an anomaly report.
+ */
+ @Test
+ public void testOutgoingCallCreatedButNotAddedNoAnomalyReport() {
+ //The call is created:
+ Call call = getCall();
+ call.setCallDirection(Call.CALL_DIRECTION_OUTGOING);
+ call.setState(CallState.NEW, "foo");
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onStartCreateConnection(call);
+
+ //The connection fails before being added to CallsManager for a known reason.
+ call.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
+
+ // Move the clock forward:
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+
+ //Ensure an anomaly report is not generated:
+ verify(mAnomalyReporterAdapter, never()).reportAnomaly(
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_UUID,
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_MSG);
+ }
+
+ /**
+ * Emulate the case where a new incoming call is created but the connection fails for a known
+ * reason before being added to CallsManager and CallsManager notifies the watchdog by invoking
+ * {@link CallsManager.CallsManagerListener#onCreateConnectionFailed(Call)}.
+ * In this case, the watchdog should stop tracking the call and not trigger an anomaly report.
+ */
+ @Test
+ public void testCallCreatedButNotAddedPreventsAnomalyReport() {
+ //The call is created:
+ Call call = getCall();
+ call.setState(CallState.NEW, "foo");
+ call.setIsCreateConnectionComplete(false);
+ mCallAnomalyWatchdog.onStartCreateConnection(call);
+
+ //Telecom cancels the connection before adding it to CallsManager:
+ mCallAnomalyWatchdog.onCreateConnectionFailed(call);
+
+ // Move the clock forward:
+ when(mMockClockProxy.elapsedRealtime()).
+ thenReturn(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+ mTestScheduledExecutorService.advanceTime(TEST_NON_VOIP_INTERMEDIATE_MILLIS + 1);
+
+ //Ensure an anomaly report is not generated:
+ verify(mAnomalyReporterAdapter, never()).reportAnomaly(
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_UUID,
+ CallAnomalyWatchdog.WATCHDOG_DISCONNECTED_STUCK_CALL_MSG);
+ }
+
+
+ /**
+ * @return an instance of {@link Call} for testing purposes.
+ */
+ private Call getCall() {
+ return new Call(
+ "1", /* callId */
+ mContext,
+ mMockCallsManager,
+ mLock,
+ null /* ConnectionServiceRepository */,
+ mMockPhoneNumberUtilsAdapter,
+ Uri.parse("tel:6505551212"),
+ null /* GatewayInfo */,
+ null /* connectionManagerPhoneAccountHandle */,
+ SIM_1_HANDLE,
+ Call.CALL_DIRECTION_INCOMING,
+ false /* shouldAttachToExistingConnection*/,
+ false /* isConference */,
+ mMockClockProxy,
+ mMockToastProxy);
+ }
+} \ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallAttributesTests.java b/tests/src/com/android/server/telecom/tests/CallAttributesTests.java
new file mode 100644
index 000000000..acb913e48
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAttributesTests.java
@@ -0,0 +1,126 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.isA;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Parcel;
+import android.telecom.CallAttributes;
+import android.telecom.PhoneAccountHandle;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class CallAttributesTests extends TelecomTestCase {
+
+ private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
+ new ComponentName("foo", "bar"), "1");
+ private static final String TEST_NAME = "Larry Page";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+ @Mock private Parcel mParcel;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testRequiredAttributes() {
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+
+ assertEquals(CallAttributes.DIRECTION_OUTGOING, callAttributes.getDirection());
+ assertEquals(mHandle, callAttributes.getPhoneAccountHandle());
+ }
+
+ @Test
+ public void testInvalidDirectionAttributes() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new CallAttributes.Builder(mHandle, -1, TEST_NAME, TEST_URI).build()
+ );
+ }
+
+ @Test
+ public void testInvalidCallType() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new CallAttributes.Builder(mHandle, CallAttributes.DIRECTION_OUTGOING,
+ TEST_NAME, TEST_URI).setCallType(-1).build()
+ );
+ }
+
+ @Test
+ public void testOptionalAttributes() {
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ assertEquals(CallAttributes.DIRECTION_OUTGOING, callAttributes.getDirection());
+ assertEquals(mHandle, callAttributes.getPhoneAccountHandle());
+ assertEquals(CallAttributes.SUPPORTS_SET_INACTIVE, callAttributes.getCallCapabilities());
+ assertEquals(CallAttributes.AUDIO_CALL, callAttributes.getCallType());
+ assertEquals(TEST_URI, callAttributes.getAddress());
+ assertEquals(TEST_NAME, callAttributes.getDisplayName());
+ }
+
+ @Test
+ public void testDescribeContents() {
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+
+ assertEquals(0, callAttributes.describeContents());
+ }
+
+ @Test
+ public void testWriteToParcel() {
+ // GIVEN
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ // WHEN
+ callAttributes.writeToParcel(mParcel, 0);
+
+ // THEN
+ verify(mParcel, times(1))
+ .writeParcelable(isA(PhoneAccountHandle.class), isA(Integer.class));
+ verify(mParcel, times(1)).writeCharSequence(isA(CharSequence.class));
+ verify(mParcel, times(1))
+ .writeParcelable(isA(Uri.class), isA(Integer.class));
+ verify(mParcel, times(3)).writeInt(isA(Integer.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 4adafc80a..3d06ad090 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -51,6 +51,7 @@ import java.util.List;
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;
@@ -677,6 +678,30 @@ public class CallAudioManagerTest extends TelecomTestCase {
assertTrue(mCallAudioManager.isCallVoip(child));
}
+ @SmallTest
+ @Test
+ public void testOnCallStreamingStateChanged() {
+ Call call = mock(Call.class);
+ ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
+
+ // non-streaming call
+ mCallAudioManager.onCallStreamingStateChanged(call, false /* isStreamingCall */);
+ verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(anyInt(),
+ any(MessageArgs.class));
+
+ // start streaming
+ mCallAudioManager.onCallStreamingStateChanged(call, true /* isStreamingCall */);
+ verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+ eq(CallAudioModeStateMachine.START_CALL_STREAMING), captor.capture());
+ assertTrue(captor.getValue().isStreaming);
+
+ // stop streaming
+ mCallAudioManager.onCallStreamingStateChanged(call, false /* isStreamingCall */);
+ verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+ eq(CallAudioModeStateMachine.STOP_CALL_STREAMING), captor.capture());
+ assertFalse(captor.getValue().isStreaming);
+ }
+
private Call createSimulatedRingingCall() {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index 38f58fd79..d7854a5a9 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -34,6 +34,7 @@ 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;
@@ -49,6 +50,7 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase {
@Mock private SystemStateHelper mSystemStateHelper;
@Mock private AudioManager mAudioManager;
@Mock private CallAudioManager mCallAudioManager;
+ @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
private HandlerThread mTestThread;
@@ -58,6 +60,8 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase {
mTestThread = new HandlerThread("CallAudioModeStateMachineTest");
mTestThread.start();
super.setUp();
+ when(mCallAudioManager.getCallAudioRouteStateMachine())
+ .thenReturn(mCallAudioRouteStateMachine);
}
@Override
@@ -102,6 +106,64 @@ public class CallAudioModeStateMachineTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testSwitchToStreamingMode() {
+ CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+ mAudioManager, mTestThread.getLooper());
+ sm.setCallAudioManager(mCallAudioManager);
+ sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+ resetMocks();
+ when(mCallAudioManager.startRinging()).thenReturn(false);
+
+ sm.sendMessage(CallAudioModeStateMachine.START_CALL_STREAMING, new Builder()
+ .setHasActiveOrDialingCalls(true)
+ .setHasRingingCalls(false)
+ .setHasHoldingCalls(false)
+ .setIsTonePlaying(false)
+ .setHasActiveOrDialingCalls(true)
+ .setForegroundCallIsVoip(true)
+ .setIsStreaming(true)
+ .setSession(null)
+ .build());
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+ assertEquals(CallAudioModeStateMachine.STREAMING_STATE_NAME, sm.getCurrentStateName());
+
+ verify(mAudioManager, never()).requestAudioFocusForCall(anyInt(), anyInt());
+ verify(mAudioManager).setMode(eq(AudioManager.MODE_COMMUNICATION_REDIRECT));
+ }
+
+ @SmallTest
+ @Test
+ public void testExitStreamingMode() {
+ CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+ mAudioManager, mTestThread.getLooper());
+ sm.setCallAudioManager(mCallAudioManager);
+ sm.sendMessage(CallAudioModeStateMachine.ENTER_STREAMING_FOCUS_FOR_TESTING);
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+ resetMocks();
+ when(mCallAudioManager.startRinging()).thenReturn(false);
+
+ sm.sendMessage(CallAudioModeStateMachine.STOP_CALL_STREAMING, new Builder()
+ .setHasActiveOrDialingCalls(false)
+ .setHasRingingCalls(false)
+ .setHasHoldingCalls(false)
+ .setIsTonePlaying(false)
+ .setForegroundCallIsVoip(true)
+ .setIsStreaming(false)
+ .setSession(null)
+ .build());
+ waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+ assertEquals(CallAudioModeStateMachine.UNFOCUSED_STATE_NAME, sm.getCurrentStateName());
+
+ verify(mAudioManager, never()).requestAudioFocusForCall(anyInt(), anyInt());
+ }
+
+ @SmallTest
+ @Test
public void testNoRingWhenDeviceIsAtEar() {
CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
mAudioManager, mTestThread.getLooper());
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
new file mode 100644
index 000000000..dfe148318
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.DockManager;
+import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+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.Mock;
+
+@RunWith(JUnit4.class)
+public class CallAudioRoutePeripheralAdapterTest extends TelecomTestCase {
+ CallAudioRoutePeripheralAdapter mAdapter;
+
+ @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ @Mock private BluetoothRouteManager mBluetoothRouteManager;
+ @Mock private WiredHeadsetManager mWiredHeadsetManager;
+ @Mock private DockManager mDockManager;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mAdapter = new CallAudioRoutePeripheralAdapter(
+ mCallAudioRouteStateMachine,
+ mBluetoothRouteManager,
+ mWiredHeadsetManager,
+ mDockManager);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testIsBluetoothAudioOn() {
+ when(mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+ assertFalse(mAdapter.isBluetoothAudioOn());
+
+ when(mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+ assertTrue(mAdapter.isBluetoothAudioOn());
+ }
+
+ @SmallTest
+ @Test
+ public void testIsHearingAidDeviceOn() {
+ when(mBluetoothRouteManager.isCachedHearingAidDevice(any())).thenReturn(false);
+ assertFalse(mAdapter.isHearingAidDeviceOn());
+
+ when(mBluetoothRouteManager.isCachedHearingAidDevice(any())).thenReturn(true);
+ assertTrue(mAdapter.isHearingAidDeviceOn());
+ }
+
+ @SmallTest
+ @Test
+ public void testIsLeAudioDeviceOn() {
+ when(mBluetoothRouteManager.isCachedLeAudioDevice(any())).thenReturn(false);
+ assertFalse(mAdapter.isLeAudioDeviceOn());
+
+ when(mBluetoothRouteManager.isCachedLeAudioDevice(any())).thenReturn(true);
+ assertTrue(mAdapter.isLeAudioDeviceOn());
+ }
+
+ @SmallTest
+ @Test
+ public void testOnBluetoothDeviceListChanged() {
+ mAdapter.onBluetoothDeviceListChanged();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnBluetoothActiveDevicePresent() {
+ mAdapter.onBluetoothActiveDevicePresent();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnBluetoothActiveDeviceGone() {
+ mAdapter.onBluetoothActiveDeviceGone();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_GONE);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnBluetoothAudioConnected() {
+ mAdapter.onBluetoothAudioConnected();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnBluetoothAudioDisconnected() {
+ mAdapter.onBluetoothAudioDisconnected();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnUnexpectedBluetoothStateChange() {
+ mAdapter.onUnexpectedBluetoothStateChange();
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnWiredHeadsetPluggedInChangedNoChange() {
+ mAdapter.onWiredHeadsetPluggedInChanged(false, false);
+ mAdapter.onWiredHeadsetPluggedInChanged(true, true);
+ verify(mCallAudioRouteStateMachine, never()).sendMessageWithSessionInfo(anyInt());
+ }
+
+ @SmallTest
+ @Test
+ public void testOnWiredHeadsetPluggedInChangedPlugged() {
+ mAdapter.onWiredHeadsetPluggedInChanged(false, true);
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnWiredHeadsetPluggedInChangedUnplugged() {
+ mAdapter.onWiredHeadsetPluggedInChanged(true, false);
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnDockChangedConnected() {
+ mAdapter.onDockChanged(true);
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.CONNECT_DOCK);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnDockChangedDisconnected() {
+ mAdapter.onDockChanged(false);
+ verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_DOCK);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 609229333..569c48750 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -53,7 +53,9 @@ import org.mockito.stubbing.Answer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -89,6 +91,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
@Mock ConnectionServiceWrapper mockConnectionServiceWrapper;
@Mock WiredHeadsetManager mockWiredHeadsetManager;
@Mock StatusBarNotifier mockStatusBarNotifier;
+ @Mock Call fakeSelfManagedCall;
@Mock Call fakeCall;
@Mock CallAudioManager mockCallAudioManager;
@@ -116,11 +119,16 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
};
when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ when(mockCallsManager.getTrackedCalls()).thenReturn(Set.of(fakeCall));
when(mockCallsManager.getLock()).thenReturn(mLock);
when(mockCallsManager.hasVideoCall()).thenReturn(false);
when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
when(fakeCall.isAlive()).thenReturn(true);
when(fakeCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
+ when(fakeSelfManagedCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
+ when(fakeSelfManagedCall.isAlive()).thenReturn(true);
+ when(fakeSelfManagedCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
+ when(fakeSelfManagedCall.isSelfManaged()).thenReturn(true);
doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
any(CallAudioState.class));
@@ -145,7 +153,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
// 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
@@ -153,6 +162,52 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
assertNotNull(stateMachine);
}
+ @SmallTest
+ @Test
+ public void testTrackedCallsReceiveAudioRouteChange() {
+ 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 */);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
+ when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls);
+
+ // 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);
+
+ // 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());
+ }
+
@MediumTest
@Test
public void testStreamRingMuteChange() {
@@ -164,7 +219,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -207,7 +263,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -252,7 +309,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -296,7 +354,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1);
@@ -374,7 +433,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -410,7 +470,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
setInBandRing(false);
when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -465,7 +526,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
List<BluetoothDevice> availableDevices =
Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3);
@@ -515,7 +577,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -546,7 +609,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
@@ -580,7 +644,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
List<BluetoothDevice> availableDevices =
Arrays.asList(bluetoothDevice1, bluetoothDevice2);
@@ -695,11 +760,44 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.initialize();
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
}
+ @SmallTest
+ @Test
+ public void testStreamingRoute() {
+ 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 */);
+ stateMachine.setCallAudioManager(mockCallAudioManager);
+
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ stateMachine.initialize(initState);
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.STREAMING_FORCE_ENABLED);
+ CallAudioState expectedEndState = new CallAudioState(false,
+ CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_STREAMING);
+
+ waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ verifyNewSystemCallAudioState(initState, expectedEndState);
+ resetMocks();
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
+ waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+ assertEquals(initState, stateMachine.getCurrentCallAudioState());
+ }
+
private void initializationTestHelper(CallAudioState expectedState,
int earpieceControl) {
when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(
@@ -717,7 +815,8 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
earpieceControl,
- mThreadHandler.getLooper());
+ mThreadHandler.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.initialize();
assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
}
@@ -749,6 +848,7 @@ public class CallAudioRouteStateMachineTest extends TelecomTestCase {
mockConnectionServiceWrapper);
fakeCall = mock(Call.class);
when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ when(mockCallsManager.getTrackedCalls()).thenReturn(Set.of(fakeCall));
when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
when(fakeCall.isAlive()).thenReturn(true);
when(fakeCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
index 3eacc3a92..cf684debb 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
@@ -64,6 +64,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
@RunWith(Parameterized.class)
public class CallAudioRouteTransitionTests extends TelecomTestCase {
@@ -182,6 +183,7 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase {
};
when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ when(mockCallsManager.getTrackedCalls()).thenReturn(Set.of(fakeCall));
when(mockCallsManager.getLock()).thenReturn(mLock);
when(mockCallsManager.hasVideoCall()).thenReturn(false);
when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
@@ -267,7 +269,8 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
mParams.earpieceControl,
- mHandlerThread.getLooper());
+ mHandlerThread.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
setupMocksForParams(stateMachine, mParams);
@@ -363,7 +366,8 @@ public class CallAudioRouteTransitionTests extends TelecomTestCase {
mockStatusBarNotifier,
mAudioServiceFactory,
mParams.earpieceControl,
- mHandlerThread.getLooper());
+ mHandlerThread.getLooper(),
+ Runnable::run /** do async stuff sync for test purposes */);
stateMachine.setCallAudioManager(mockCallAudioManager);
// Set up bluetooth and speakerphone state
diff --git a/tests/src/com/android/server/telecom/tests/CallControlTest.java b/tests/src/com/android/server/telecom/tests/CallControlTest.java
new file mode 100644
index 000000000..2613206c7
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallControlTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.os.OutcomeReceiver;
+import android.telecom.CallControl;
+import android.telecom.CallException;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.telecom.ClientTransactionalServiceRepository;
+import com.android.internal.telecom.ICallControl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+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;
+ private static final String CALL_ID_1 = UUID.randomUUID().toString();
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testGetCallId() {
+ CallControl control = new CallControl(CALL_ID_1, mICallControl, mRepository, mHandle);
+ 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
new file mode 100644
index 000000000..f4008aa83
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 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.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+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.BluetoothDevice;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
+import android.test.mock.MockContext;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallEndpointController;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceWrapper;
+
+import org.junit.Before;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class CallEndpointControllerTest extends TelecomTestCase {
+ private static final BluetoothDevice bluetoothDevice1 =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
+ private static final BluetoothDevice bluetoothDevice2 =
+ BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+ private static final Collection<BluetoothDevice> availableBluetooth1 =
+ Arrays.asList(bluetoothDevice1, bluetoothDevice2);
+ private static final Collection<BluetoothDevice> availableBluetooth2 =
+ Arrays.asList(bluetoothDevice1);
+
+ private static final CallAudioState audioState1 = new CallAudioState(false,
+ CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+ private static final CallAudioState audioState2 = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+ availableBluetooth1);
+ private static final CallAudioState audioState3 = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL, bluetoothDevice2,
+ availableBluetooth1);
+ private static final CallAudioState audioState4 = new CallAudioState(false,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+ availableBluetooth2);
+ private static final CallAudioState audioState5 = new CallAudioState(true,
+ CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+ private static final CallAudioState audioState6 = new CallAudioState(false,
+ CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_EARPIECE, null,
+ availableBluetooth1);
+ private static final CallAudioState audioState7 = new CallAudioState(false,
+ CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+
+ private CallEndpointController mCallEndpointController;
+
+ @Mock private CallsManager mCallsManager;
+ @Mock private Call mCall;
+ @Mock private ConnectionServiceWrapper mConnectionService;
+ @Mock private CallAudioManager mCallAudioManager;
+ @Mock private MockContext mMockContext;
+ @Mock private ResultReceiver mResultReceiver;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mCallEndpointController = new CallEndpointController(mMockContext, mCallsManager);
+ doReturn(new HashSet<>(Arrays.asList(mCall))).when(mCallsManager).getTrackedCalls();
+ doReturn(mConnectionService).when(mCall).getConnectionService();
+ doReturn(mCallAudioManager).when(mCallsManager).getCallAudioManager();
+ when(mMockContext.getText(R.string.callendpoint_name_earpiece)).thenReturn("Earpiece");
+ when(mMockContext.getText(R.string.callendpoint_name_bluetooth)).thenReturn("Bluetooth");
+ when(mMockContext.getText(R.string.callendpoint_name_wiredheadset))
+ .thenReturn("Wired headset");
+ when(mMockContext.getText(R.string.callendpoint_name_speaker)).thenReturn("Speaker");
+ when(mMockContext.getText(R.string.callendpoint_name_streaming)).thenReturn("External");
+ when(mMockContext.getText(R.string.callendpoint_name_unknown)).thenReturn("Unknown");
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testCurrentEndpointChangedToBluetooth() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState1, audioState2);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+ String bluetoothAddress = mCallEndpointController.getBluetoothAddress(endpoint);
+
+ // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+ assertEquals(5, availableEndpoints.size());
+ // type of current CallEndpoint is Bluetooth
+ assertEquals(CallEndpoint.TYPE_BLUETOOTH, endpoint.getEndpointType());
+ assertEquals(bluetoothDevice1.getAddress(), bluetoothAddress);
+
+ verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+ verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+ verify(mCallsManager, never()).updateAvailableCallEndpoints(any());
+ verify(mConnectionService, never()).onAvailableCallEndpointsChanged(any(), any());
+ verify(mCallsManager, never()).updateMuteState(anyBoolean());
+ verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+ }
+
+ @Test
+ public void testCurrentEndpointChangedToStreaming() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState1, audioState7);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+ // Only Streaming is available, but it will not be reported via the available endpoints list
+ assertEquals(0, availableEndpoints.size());
+ assertNotNull(availableEndpoints);
+ // type of current CallEndpoint is Streaming
+ assertEquals(CallEndpoint.TYPE_STREAMING, endpoint.getEndpointType());
+
+ verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+ verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+ verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+ verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+ eq(availableEndpoints));
+ verify(mCallsManager, never()).updateMuteState(anyBoolean());
+ verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+ }
+
+ @Test
+ public void testCurrentEndpointChangedBetweenBluetooth() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState2, audioState3);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+ String bluetoothAddress = mCallEndpointController.getBluetoothAddress(endpoint);
+
+ // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+ assertEquals(5, availableEndpoints.size());
+ // type of current CallEndpoint is Bluetooth
+ assertEquals(CallEndpoint.TYPE_BLUETOOTH, endpoint.getEndpointType());
+ assertEquals(bluetoothDevice2.getAddress(), bluetoothAddress);
+
+ verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+ verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+ verify(mCallsManager, never()).updateAvailableCallEndpoints(any());
+ verify(mConnectionService, never()).onAvailableCallEndpointsChanged(any(), any());
+ verify(mCallsManager, never()).updateMuteState(anyBoolean());
+ verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+ }
+
+ @Test
+ public void testAvailableEndpointChanged() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState1, audioState6);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+ // Only Earpiece is available
+ assertEquals(1, availableEndpoints.size());
+ // type of current CallEndpoint is Earpiece
+ assertEquals(CallEndpoint.TYPE_EARPIECE, endpoint.getEndpointType());
+ assertTrue(availableEndpoints.contains(endpoint));
+
+ verify(mCallsManager, never()).updateCallEndpoint(any());
+ verify(mConnectionService, never()).onCallEndpointChanged(any(), any());
+ verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+ verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+ eq(availableEndpoints));
+ verify(mCallsManager, never()).updateMuteState(anyBoolean());
+ verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+ }
+
+ @Test
+ public void testAvailableBluetoothEndpointChanged() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState2, audioState4);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+ String bluetoothAddress = mCallEndpointController.getBluetoothAddress(endpoint);
+
+ // Earpiece, Wired headset, Speaker and one Bluetooth endpoint is available
+ assertEquals(4, availableEndpoints.size());
+ // type of current CallEndpoint is Bluetooth
+ assertEquals(CallEndpoint.TYPE_BLUETOOTH, endpoint.getEndpointType());
+ assertEquals(bluetoothDevice1.getAddress(), bluetoothAddress);
+
+ verify(mCallsManager, never()).updateCallEndpoint(any());
+ verify(mConnectionService, never()).onCallEndpointChanged(any(), any());
+ verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+ verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+ eq(availableEndpoints));
+ verify(mCallsManager, never()).updateMuteState(anyBoolean());
+ verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+ }
+
+ @Test
+ public void testMuteStateChanged() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState1, audioState5);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+ // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+ assertEquals(5, availableEndpoints.size());
+ // type of current CallEndpoint is Earpiece
+ assertEquals(CallEndpoint.TYPE_EARPIECE, endpoint.getEndpointType());
+
+ verify(mCallsManager, never()).updateCallEndpoint(any());
+ verify(mConnectionService, never()).onCallEndpointChanged(any(), any());
+ verify(mCallsManager, never()).updateAvailableCallEndpoints(any());
+ verify(mConnectionService, never()).onAvailableCallEndpointsChanged(any(), any());
+ verify(mCallsManager).updateMuteState(eq(true));
+ verify(mConnectionService, times(1)).onMuteStateChanged(eq(mCall), eq(true));
+ }
+
+ @Test
+ public void testNotifyForcely() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(audioState1, audioState1);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+ // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+ assertEquals(5, availableEndpoints.size());
+ // type of current CallEndpoint is Earpiece
+ assertEquals(CallEndpoint.TYPE_EARPIECE, endpoint.getEndpointType());
+
+ verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+ verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+ verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+ verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+ eq(availableEndpoints));
+ verify(mCallsManager).updateMuteState(eq(false));
+ verify(mConnectionService, times(1)).onMuteStateChanged(eq(mCall), eq(false));
+ }
+
+ @Test
+ public void testEndpointChangeRequest() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(null, audioState1);
+ CallEndpoint endpoint1 = mCallEndpointController.getCurrentCallEndpoint();
+
+ mCallEndpointController.onCallAudioStateChanged(audioState1, audioState2);
+ CallEndpoint endpoint2 = mCallEndpointController.getCurrentCallEndpoint();
+
+ mCallEndpointController.requestCallEndpointChange(endpoint1, mResultReceiver);
+ verify(mCallAudioManager).setAudioRoute(eq(CallAudioState.ROUTE_EARPIECE), eq(null));
+
+ mCallEndpointController.requestCallEndpointChange(endpoint2, mResultReceiver);
+ verify(mCallAudioManager).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+ eq(bluetoothDevice1.getAddress()));
+ }
+
+ @Test
+ public void testEndpointChangeRequest_EndpointDoesNotExist() throws Exception {
+ mCallEndpointController.onCallAudioStateChanged(null, audioState2);
+ CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+ mCallEndpointController.onCallAudioStateChanged(audioState2, audioState6);
+
+ mCallEndpointController.requestCallEndpointChange(endpoint, mResultReceiver);
+ verify(mCallAudioManager, never()).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+ eq(bluetoothDevice1.getAddress()));
+ verify(mResultReceiver).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+ }
+} \ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallExceptionTests.java b/tests/src/com/android/server/telecom/tests/CallExceptionTests.java
new file mode 100644
index 000000000..fa2287793
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallExceptionTests.java
@@ -0,0 +1,82 @@
+/*
+ * 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 android.telecom.CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE;
+import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
+
+import static org.junit.Assert.assertTrue;
+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 android.os.Parcel;
+import android.telecom.CallException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class CallExceptionTests extends TelecomTestCase {
+
+ @Mock private Parcel mParcel;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testExceptionWithCode() {
+ String message = "test message";
+ CallException exception = new CallException(message, CODE_CALL_CANNOT_BE_SET_TO_ACTIVE);
+ assertTrue(exception.getMessage().contains(message));
+ assertEquals(CODE_CALL_CANNOT_BE_SET_TO_ACTIVE, exception.getCode());
+ }
+
+ @Test
+ public void testDescribeContents() {
+ String message = "test message";
+ CallException exception = new CallException(message, CODE_ERROR_UNKNOWN);
+ assertEquals(0, exception.describeContents());
+ }
+
+ @Test
+ public void testWriteToParcel() {
+ // GIVEN
+ String message = "test message";
+ CallException exception = new CallException(message, CODE_ERROR_UNKNOWN);
+
+ // WHEN
+ exception.writeToParcel(mParcel, 0);
+
+ // THEN
+ verify(mParcel, times(1)).writeString8(isA(String.class));
+ verify(mParcel, times(1)).writeInt(isA(Integer.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
index 926d74078..cf44cfeff 100644
--- a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
@@ -370,7 +370,7 @@ public class CallExtrasTest extends TelecomSystemTest {
@LargeTest
@Test
public void testConferenceSetExtras() throws Exception {
- ParcelableCall call = makeConferenceCall();
+ ParcelableCall call = makeConferenceCall(null, null);
String conferenceId = call.getId();
Conference conference = mConnectionServiceFixtureA.mLatestConference;
@@ -414,7 +414,7 @@ public class CallExtrasTest extends TelecomSystemTest {
@FlakyTest(bugId = 117751305)
@Test
public void testConferenceExtraOperations() throws Exception {
- ParcelableCall call = makeConferenceCall();
+ ParcelableCall call = makeConferenceCall(null, null);
String conferenceId = call.getId();
Conference conference = mConnectionServiceFixtureA.mLatestConference;
assertNotNull(conference);
@@ -450,7 +450,7 @@ public class CallExtrasTest extends TelecomSystemTest {
@LargeTest
@Test
public void testConferenceICS() throws Exception {
- ParcelableCall call = makeConferenceCall();
+ ParcelableCall call = makeConferenceCall(null, null);
String conferenceId = call.getId();
Conference conference = mConnectionServiceFixtureA.mLatestConference;
diff --git a/tests/src/com/android/server/telecom/tests/CallFailureCauseTest.java b/tests/src/com/android/server/telecom/tests/CallFailureCauseTest.java
new file mode 100644
index 000000000..7c0f649bf
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallFailureCauseTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.server.telecom.stats.CallFailureCause;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CallFailureCauseTest extends TelecomTestCase {
+ @Parameters
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+ {CallFailureCause.NONE, 0, true},
+ {CallFailureCause.INVALID_USE, 1, false},
+ {CallFailureCause.IN_EMERGENCY_CALL, 2, false},
+ {CallFailureCause.CANNOT_HOLD_CALL, 3, false},
+ {CallFailureCause.MAX_OUTGOING_CALLS, 4, false},
+ {CallFailureCause.MAX_RINGING_CALLS, 5, false},
+ {CallFailureCause.MAX_HOLD_CALLS, 6, false},
+ {CallFailureCause.MAX_SELF_MANAGED_CALLS, 7, false},
+ });
+ }
+ @Parameter(0) public CallFailureCause e;
+ @Parameter(1) public int code;
+ @Parameter(2) public boolean isSuccess;
+
+ @Test
+ public void testGetCode() {
+ assertEquals(code, e.getCode());
+ }
+
+ @Test
+ public void testIsSuccess() {
+ assertEquals(isSuccess, e.isSuccess());
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index b9f5667ab..946622030 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -1062,7 +1062,7 @@ public class CallLogManagerTest extends TelecomTestCase {
when(fakeCall.getVideoStateHistory()).thenReturn(callVideoState);
when(fakeCall.getPostDialDigits()).thenReturn(postDialDigits);
when(fakeCall.getViaNumber()).thenReturn(viaNumber);
- when(fakeCall.getInitiatingUser()).thenReturn(initiatingUser);
+ when(fakeCall.getAssociatedUser()).thenReturn(initiatingUser);
when(fakeCall.getCallDataUsage()).thenReturn(callDataUsage);
when(fakeCall.isEmergencyCall()).thenReturn(
phoneAccountHandle.equals(EMERGENCY_ACCT_HANDLE));
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index f2fe045ef..01446d158 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -153,8 +153,9 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
}
private void enableUserDefinedCallRedirectionService() {
- when(mCallRedirectionProcessorHelper.getUserDefinedCallRedirectionService()).thenReturn(
- USER_DEFINED_SERVICE_TEST_COMPONENT_NAME);
+ when(mCallRedirectionProcessorHelper
+ .getUserDefinedCallRedirectionService(any(UserHandle.class)))
+ .thenReturn(USER_DEFINED_SERVICE_TEST_COMPONENT_NAME);
}
private void enableCarrierCallRedirectionService() {
@@ -163,8 +164,9 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
}
private void disableUserDefinedCallRedirectionService() {
- when(mCallRedirectionProcessorHelper.getUserDefinedCallRedirectionService()).thenReturn(
- null);
+ when(mCallRedirectionProcessorHelper
+ .getUserDefinedCallRedirectionService(any(UserHandle.class)))
+ .thenReturn(null);
}
private void disableCarrierCallRedirectionService() {
@@ -193,9 +195,9 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
startProcessWithNoGateWayInfo();
disableUserDefinedCallRedirectionService();
disableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
verify(mContext, times(0)).bindServiceAsUser(any(Intent.class),
- any(ServiceConnection.class), anyInt(), any(UserHandle.class));
+ any(ServiceConnection.class), anyInt(), eq(UserHandle.CURRENT));
verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), eq(mHandle),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(false), eq(CallRedirectionProcessor.UI_TYPE_NO_ACTION));
@@ -208,9 +210,9 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
waitForHandlerAction(mProcessor.getHandler(), HANDLER_TIMEOUT_DELAY);
disableUserDefinedCallRedirectionService();
enableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
verify(mContext, times(1)).bindServiceAsUser(any(Intent.class),
- any(ServiceConnection.class), anyInt(), any(UserHandle.class));
+ any(ServiceConnection.class), anyInt(), eq(UserHandle.CURRENT));
verify(mCallsManager, times(0)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(false), eq(CallRedirectionProcessor.UI_TYPE_NO_ACTION));
@@ -228,9 +230,9 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
waitForHandlerAction(mProcessor.getHandler(), HANDLER_TIMEOUT_DELAY);
enableUserDefinedCallRedirectionService();
disableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
verify(mContext, times(1)).bindServiceAsUser(any(Intent.class),
- any(ServiceConnection.class), anyInt(), any(UserHandle.class));
+ any(ServiceConnection.class), anyInt(), eq(UserHandle.CURRENT));
verify(mCallsManager, times(0)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(false), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
@@ -256,10 +258,10 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
waitForHandlerAction(mProcessor.getHandler(), HANDLER_TIMEOUT_DELAY);
enableUserDefinedCallRedirectionService();
enableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
verify(mContext, times(1)).bindServiceAsUser(any(Intent.class),
- any(ServiceConnection.class), anyInt(), any(UserHandle.class));
+ any(ServiceConnection.class), anyInt(), eq(UserHandle.CURRENT));
verify(mCallsManager, times(0)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(false), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
@@ -294,9 +296,9 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
startProcessWithGateWayInfo();
enableUserDefinedCallRedirectionService();
enableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
verify(mContext, times(1)).bindServiceAsUser(any(Intent.class),
- any(ServiceConnection.class), anyInt(), any(UserHandle.class));
+ any(ServiceConnection.class), anyInt(), eq(UserHandle.CURRENT));
verify(mCallsManager, times(0)).onCallRedirectionComplete(eq(mCall), any(),
eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
eq(false), eq(CallRedirectionProcessor.UI_TYPE_NO_ACTION));
@@ -313,13 +315,13 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
enableUserDefinedCallRedirectionService();
disableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
// Capture binding and mock it out.
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor = ArgumentCaptor.forClass(
ServiceConnection.class);
verify(mContext, times(1)).bindServiceAsUser(any(Intent.class),
- serviceConnectionCaptor.capture(), anyInt(), any(UserHandle.class));
+ serviceConnectionCaptor.capture(), anyInt(), eq(UserHandle.CURRENT));
// Mock out a service which performed a redirection
IBinder mockBinder = mock(IBinder.class);
@@ -361,7 +363,7 @@ public class CallRedirectionProcessorTest extends TelecomTestCase {
enableUserDefinedCallRedirectionService();
disableCarrierCallRedirectionService();
- mProcessor.performCallRedirection();
+ mProcessor.performCallRedirection(UserHandle.CURRENT);
// Capture the binder
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor = ArgumentCaptor.forClass(
diff --git a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index 09ba47e5c..4d8d497e6 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -41,6 +41,7 @@ import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.CallScreeningService;
import android.telecom.ParcelableCall;
+import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.test.suitebuilder.annotation.SmallTest;
@@ -104,6 +105,10 @@ public class CallScreeningServiceFilterTest extends TelecomTestCase {
.setCallScreeningComponentName(COMPONENT_NAME.flattenToString())
.build();
+ private static final PhoneAccountHandle PA_HANDLE =
+ new PhoneAccountHandle(COMPONENT_NAME,
+ "pa_id");
+
@Override
@Before
public void setUp() throws Exception {
@@ -124,6 +129,8 @@ public class CallScreeningServiceFilterTest extends TelecomTestCase {
when(mCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
when(mCall.getId()).thenReturn(CALL_ID);
+ when(mCall.getAssociatedUser()).
+ thenReturn(PA_HANDLE.getUserHandle());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mContext.getSystemService(TelecomManager.class))
.thenReturn(mTelecomManager);
@@ -132,7 +139,7 @@ public class CallScreeningServiceFilterTest extends TelecomTestCase {
when(mParcelableCallUtilsConverter.toParcelableCall(
eq(mCall), anyBoolean(), eq(mPhoneAccountRegistrar))).thenReturn(null);
when(mContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class),
- anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
+ anyInt(), eq(PA_HANDLE.getUserHandle()))).thenReturn(true);
when(mPackageManager.queryIntentServicesAsUser(nullable(Intent.class), anyInt(), anyInt()))
.thenReturn(Collections.singletonList(mResolveInfo));
doReturn(mCallScreeningService).when(mBinder).queryLocalInterface(anyString());
@@ -154,7 +161,7 @@ public class CallScreeningServiceFilterTest extends TelecomTestCase {
@Test
public void testContextFailToBind() throws Exception {
when(mContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class),
- anyInt(), eq(UserHandle.CURRENT))).thenReturn(false);
+ anyInt(), eq(PA_HANDLE.getUserHandle()))).thenReturn(false);
CallScreeningServiceFilter filter = new CallScreeningServiceFilter(mCall, PKG_NAME,
CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, mCallsManager,
mAppLabelProxy, mParcelableCallUtilsConverter);
@@ -199,7 +206,7 @@ public class CallScreeningServiceFilterTest extends TelecomTestCase {
when(mPackageManager.checkPermission(Manifest.permission.READ_CONTACTS, PKG_NAME))
.thenReturn(PackageManager.PERMISSION_DENIED);
when(mContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class),
- anyInt(), eq(UserHandle.CURRENT))).thenThrow(new SecurityException());
+ anyInt(), eq(PA_HANDLE.getUserHandle()))).thenThrow(new SecurityException());
inputResult.contactExists = true;
CallScreeningServiceFilter filter = new CallScreeningServiceFilter(mCall, PKG_NAME,
CallScreeningServiceFilter.PACKAGE_TYPE_USER_CHOSEN, mContext, mCallsManager,
@@ -397,7 +404,7 @@ public class CallScreeningServiceFilterTest extends TelecomTestCase {
.bindServiceAsUser(intentCaptor.capture(), serviceCaptor.capture(),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_SCHEDULE_LIKE_TOP_APP),
- eq(UserHandle.CURRENT));
+ eq(PA_HANDLE.getUserHandle()));
Intent capturedIntent = intentCaptor.getValue();
assertEquals(CallScreeningService.SERVICE_INTERFACE, capturedIntent.getAction());
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index d326a2950..997e7dd2a 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -18,51 +18,66 @@ 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.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
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.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.CallAttributes;
+import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
+import android.telecom.ParcelableConference;
+import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.CallQuality;
import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Log;
import android.widget.Toast;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIdMapper;
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.InCallController;
-import com.android.server.telecom.InCallController.InCallServiceInfo;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.TransactionalServiceWrapper;
import com.android.server.telecom.ui.ToastFactory;
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 org.mockito.Mockito;
+import java.util.Collections;
+
@RunWith(AndroidJUnit4.class)
public class CallTest extends TelecomTestCase {
private static final Uri TEST_ADDRESS = Uri.parse("tel:555-1212");
@@ -87,6 +102,7 @@ public class CallTest extends TelecomTestCase {
@Mock private Toast mMockToast;
@Mock private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
@Mock private ConnectionServiceWrapper mMockConnectionService;
+ @Mock private TransactionalServiceWrapper mMockTransactionalService;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
@@ -95,6 +111,7 @@ public class CallTest extends TelecomTestCase {
super.setUp();
doReturn(mMockCallerInfoLookupHelper).when(mMockCallsManager).getCallerInfoLookupHelper();
doReturn(mMockPhoneAccountRegistrar).when(mMockCallsManager).getPhoneAccountRegistrar();
+ doReturn(0L).when(mMockClockProxy).elapsedRealtime();
doReturn(SIM_1_ACCOUNT).when(mMockPhoneAccountRegistrar).getPhoneAccountUnchecked(
eq(SIM_1_HANDLE));
doReturn(new ComponentName(mContext, CallTest.class))
@@ -110,23 +127,7 @@ public class CallTest extends TelecomTestCase {
@Test
@SmallTest
public void testSetHasGoneActive() {
- Call call = new Call(
- "1", /* callId */
- mContext,
- mMockCallsManager,
- mLock,
- null /* ConnectionServiceRepository */,
- mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
- null /* GatewayInfo */,
- null /* connectionManagerPhoneAccountHandle */,
- SIM_1_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
- false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
- mMockClockProxy,
- mMockToastProxy);
-
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
assertFalse(call.hasGoneActiveBefore());
call.setState(CallState.ACTIVE, "");
assertTrue(call.hasGoneActiveBefore());
@@ -134,9 +135,56 @@ public class CallTest extends TelecomTestCase {
assertTrue(call.hasGoneActiveBefore());
}
+ /**
+ * Basic tests to check which call states are considered transitory.
+ */
@Test
@SmallTest
- public void testDisconnectCauseWhenAudioProcessing() {
+ public void testIsCallStateTransitory() {
+ assertTrue(CallState.isTransitoryState(CallState.NEW));
+ assertTrue(CallState.isTransitoryState(CallState.CONNECTING));
+ assertTrue(CallState.isTransitoryState(CallState.DISCONNECTING));
+ assertTrue(CallState.isTransitoryState(CallState.ANSWERED));
+
+ assertFalse(CallState.isTransitoryState(CallState.SELECT_PHONE_ACCOUNT));
+ assertFalse(CallState.isTransitoryState(CallState.DIALING));
+ assertFalse(CallState.isTransitoryState(CallState.RINGING));
+ assertFalse(CallState.isTransitoryState(CallState.ACTIVE));
+ assertFalse(CallState.isTransitoryState(CallState.ON_HOLD));
+ assertFalse(CallState.isTransitoryState(CallState.DISCONNECTED));
+ assertFalse(CallState.isTransitoryState(CallState.ABORTED));
+ assertFalse(CallState.isTransitoryState(CallState.PULLING));
+ assertFalse(CallState.isTransitoryState(CallState.AUDIO_PROCESSING));
+ assertFalse(CallState.isTransitoryState(CallState.SIMULATED_RINGING));
+ }
+
+ /**
+ * Basic tests to check which call states are considered intermediate.
+ */
+ @Test
+ @SmallTest
+ public void testIsCallStateIntermediate() {
+ assertTrue(CallState.isIntermediateState(CallState.DIALING));
+ assertTrue(CallState.isIntermediateState(CallState.RINGING));
+
+ assertFalse(CallState.isIntermediateState(CallState.NEW));
+ assertFalse(CallState.isIntermediateState(CallState.CONNECTING));
+ assertFalse(CallState.isIntermediateState(CallState.DISCONNECTING));
+ assertFalse(CallState.isIntermediateState(CallState.ANSWERED));
+ assertFalse(CallState.isIntermediateState(CallState.SELECT_PHONE_ACCOUNT));
+ assertFalse(CallState.isIntermediateState(CallState.ACTIVE));
+ assertFalse(CallState.isIntermediateState(CallState.ON_HOLD));
+ assertFalse(CallState.isIntermediateState(CallState.DISCONNECTED));
+ assertFalse(CallState.isIntermediateState(CallState.ABORTED));
+ assertFalse(CallState.isIntermediateState(CallState.PULLING));
+ assertFalse(CallState.isIntermediateState(CallState.AUDIO_PROCESSING));
+ assertFalse(CallState.isIntermediateState(CallState.SIMULATED_RINGING));
+ }
+
+ @SmallTest
+ @Test
+ public void testIsCreateConnectionComplete() {
+ // A new call with basic info.
Call call = new Call(
"1", /* callId */
mContext,
@@ -153,6 +201,22 @@ public class CallTest extends TelecomTestCase {
false /* isConference */,
mMockClockProxy,
mMockToastProxy);
+
+ // To start with connection creation isn't complete.
+ assertFalse(call.isCreateConnectionComplete());
+
+ // Need the bare minimum to get connection creation to complete.
+ ParcelableConnection connection = new ParcelableConnection(null, 0, 0, 0, 0, null, 0, null,
+ 0, null, 0, false, false, 0L, 0L, null, null, Collections.emptyList(), null, null,
+ 0, 0);
+ call.handleCreateConnectionSuccess(Mockito.mock(CallIdMapper.class), connection);
+ assertTrue(call.isCreateConnectionComplete());
+ }
+
+ @Test
+ @SmallTest
+ public void testDisconnectCauseWhenAudioProcessing() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
call.setState(CallState.AUDIO_PROCESSING, "");
call.disconnect();
call.setDisconnectCause(new DisconnectCause(DisconnectCause.LOCAL));
@@ -162,22 +226,7 @@ public class CallTest extends TelecomTestCase {
@Test
@SmallTest
public void testDisconnectCauseWhenAudioProcessingAfterActive() {
- Call call = new Call(
- "1", /* callId */
- mContext,
- mMockCallsManager,
- mLock,
- null /* ConnectionServiceRepository */,
- mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
- null /* GatewayInfo */,
- null /* connectionManagerPhoneAccountHandle */,
- SIM_1_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
- false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
- mMockClockProxy,
- mMockToastProxy);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
call.setState(CallState.AUDIO_PROCESSING, "");
call.setState(CallState.ACTIVE, "");
call.setState(CallState.AUDIO_PROCESSING, "");
@@ -189,22 +238,7 @@ public class CallTest extends TelecomTestCase {
@Test
@SmallTest
public void testDisconnectCauseWhenSimulatedRingingAndDisconnect() {
- Call call = new Call(
- "1", /* callId */
- mContext,
- mMockCallsManager,
- mLock,
- null /* ConnectionServiceRepository */,
- mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
- null /* GatewayInfo */,
- null /* connectionManagerPhoneAccountHandle */,
- SIM_1_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
- false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
- mMockClockProxy,
- mMockToastProxy);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
call.setState(CallState.SIMULATED_RINGING, "");
call.disconnect();
call.setDisconnectCause(new DisconnectCause(DisconnectCause.LOCAL));
@@ -214,22 +248,7 @@ public class CallTest extends TelecomTestCase {
@Test
@SmallTest
public void testDisconnectCauseWhenSimulatedRingingAndReject() {
- Call call = new Call(
- "1", /* callId */
- mContext,
- mMockCallsManager,
- mLock,
- null /* ConnectionServiceRepository */,
- mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
- null /* GatewayInfo */,
- null /* connectionManagerPhoneAccountHandle */,
- SIM_1_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
- false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
- mMockClockProxy,
- mMockToastProxy);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
call.setState(CallState.SIMULATED_RINGING, "");
call.reject(false, "");
call.setDisconnectCause(new DisconnectCause(DisconnectCause.LOCAL));
@@ -239,22 +258,7 @@ public class CallTest extends TelecomTestCase {
@Test
@SmallTest
public void testCanPullCallRemovedDuringEmergencyCall() {
- Call call = new Call(
- "1", /* callId */
- mContext,
- mMockCallsManager,
- mLock,
- null /* ConnectionServiceRepository */,
- mMockPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
- null /* GatewayInfo */,
- null /* connectionManagerPhoneAccountHandle */,
- SIM_1_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
- false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
- mMockClockProxy,
- mMockToastProxy);
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
boolean[] hasCalledConnectionCapabilitiesChanged = new boolean[1];
call.addListener(new Call.ListenerBase() {
@Override
@@ -287,6 +291,38 @@ public class CallTest extends TelecomTestCase {
@Test
@SmallTest
public void testCanNotPullCallDuringEmergencyCall() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ call.setConnectionService(mMockConnectionService);
+ call.setConnectionProperties(Connection.PROPERTY_IS_EXTERNAL_CALL);
+ call.setConnectionCapabilities(Connection.CAPABILITY_CAN_PULL_CALL);
+ call.setState(CallState.ACTIVE, "");
+ // Emergency call in progress, this should show a toast and never call pullExternalCall
+ // on the ConnectionService.
+ doReturn(true).when(mMockCallsManager).isInEmergencyCall();
+ call.pullExternalCall();
+ verify(mMockConnectionService, never()).pullExternalCall(any());
+ verify(mMockToast).show();
+ }
+
+ @Test
+ @SmallTest
+ public void testCallDirection() {
+ Call call = createCall("1");
+ boolean[] hasCallDirectionChanged = new boolean[1];
+ call.addListener(new Call.ListenerBase() {
+ @Override
+ public void onCallDirectionChanged(Call call) {
+ hasCallDirectionChanged[0] = true;
+ }
+ });
+ assertFalse(call.isIncoming());
+ call.setCallDirection(Call.CALL_DIRECTION_INCOMING);
+ assertTrue(hasCallDirectionChanged[0]);
+ assertTrue(call.isIncoming());
+ }
+
+ @Test
+ public void testIsSuppressedByDoNotDisturbExtra() {
Call call = new Call(
"1", /* callId */
mContext,
@@ -298,26 +334,51 @@ public class CallTest extends TelecomTestCase {
null /* GatewayInfo */,
null /* connectionManagerPhoneAccountHandle */,
SIM_1_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
+ Call.CALL_DIRECTION_UNDEFINED,
false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
+ true /* isConference */,
mMockClockProxy,
mMockToastProxy);
+
+ assertFalse(call.wasDndCheckComputedForCall());
+ assertFalse(call.isCallSuppressedByDoNotDisturb());
+ call.setCallIsSuppressedByDoNotDisturb(true);
+ assertTrue(call.wasDndCheckComputedForCall());
+ assertTrue(call.isCallSuppressedByDoNotDisturb());
+ }
+
+ @Test
+ public void testGetConnectionServiceWrapper() {
+ Call call = new Call(
+ "1", /* callId */
+ mContext,
+ mMockCallsManager,
+ mLock,
+ null /* ConnectionServiceRepository */,
+ mMockPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null /* connectionManagerPhoneAccountHandle */,
+ SIM_1_HANDLE,
+ Call.CALL_DIRECTION_UNDEFINED,
+ false /* shouldAttachToExistingConnection*/,
+ true /* isConference */,
+ mMockClockProxy,
+ mMockToastProxy);
+
+ assertNull(call.getConnectionServiceWrapper());
+ assertFalse(call.isTransactionalCall());
call.setConnectionService(mMockConnectionService);
- call.setConnectionProperties(Connection.PROPERTY_IS_EXTERNAL_CALL);
- call.setConnectionCapabilities(Connection.CAPABILITY_CAN_PULL_CALL);
- call.setState(CallState.ACTIVE, "");
- // Emergency call in progress, this should show a toast and never call pullExternalCall
- // on the ConnectionService.
- doReturn(true).when(mMockCallsManager).isInEmergencyCall();
- call.pullExternalCall();
- verify(mMockConnectionService, never()).pullExternalCall(any());
- verify(mMockToast).show();
+ assertEquals(mMockConnectionService, call.getConnectionServiceWrapper());
+ call.setIsTransactionalCall(true);
+ assertTrue(call.isTransactionalCall());
+ assertNull(call.getConnectionServiceWrapper());
+ call.setTransactionServiceWrapper(mMockTransactionalService);
+ assertEquals(mMockTransactionalService, call.getTransactionServiceWrapper());
}
@Test
- @SmallTest
- public void testCallDirection() {
+ public void testCallEventCallbacksWereCalled() {
Call call = new Call(
"1", /* callId */
mContext,
@@ -334,16 +395,359 @@ public class CallTest extends TelecomTestCase {
true /* isConference */,
mMockClockProxy,
mMockToastProxy);
- boolean[] hasCallDirectionChanged = new boolean[1];
- call.addListener(new Call.ListenerBase() {
- @Override
- public void onCallDirectionChanged(Call call) {
- hasCallDirectionChanged[0] = true;
- }
- });
- assertFalse(call.isIncoming());
- call.setCallDirection(Call.CALL_DIRECTION_INCOMING);
- assertTrue(hasCallDirectionChanged[0]);
- assertTrue(call.isIncoming());
+
+ // setup
+ call.setIsTransactionalCall(true);
+ assertTrue(call.isTransactionalCall());
+ assertNull(call.getConnectionServiceWrapper());
+ call.setTransactionServiceWrapper(mMockTransactionalService);
+ assertEquals(mMockTransactionalService, call.getTransactionServiceWrapper());
+
+ // assert CallEventCallback#onSetInactive is called
+ call.setState(CallState.ACTIVE, "test");
+ call.hold();
+ verify(mMockTransactionalService, times(1)).onSetInactive(call);
+
+ // assert CallEventCallback#onSetActive is called
+ call.setState(CallState.ON_HOLD, "test");
+ call.unhold();
+ verify(mMockTransactionalService, times(1)).onSetActive(call);
+
+ // assert CallEventCallback#onAnswer is called
+ call.setState(CallState.RINGING, "test");
+ call.answer(0);
+ verify(mMockTransactionalService, times(1)).onAnswer(call, 0);
+
+ // assert CallEventCallback#onDisconnect is called
+ call.setState(CallState.ACTIVE, "test");
+ call.disconnect();
+ verify(mMockTransactionalService, times(1)).onDisconnect(call,
+ call.getDisconnectCause());
+ }
+
+ @Test
+ @SmallTest
+ public void testSetConnectionPropertiesRttOnOff() {
+ Call call = createCall("1");
+ call.setConnectionService(mMockConnectionService);
+
+ call.setConnectionProperties(Connection.PROPERTY_IS_RTT);
+ verify(mMockCallsManager).playRttUpgradeToneForCall(any());
+ assertNotNull(null, call.getInCallToCsRttPipeForCs());
+ assertNotNull(null, call.getCsToInCallRttPipeForInCall());
+
+ call.setConnectionProperties(0);
+ assertNull(null, call.getInCallToCsRttPipeForCs());
+ assertNull(null, call.getCsToInCallRttPipeForInCall());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetFromCallerInfo() {
+ Call call = createCall("1");
+
+ CallerInfo info = new CallerInfo();
+ info.setName("name");
+ info.setPhoneNumber("number");
+ info.cachedPhoto = new ColorDrawable();
+ info.cachedPhotoIcon = Bitmap.createBitmap(24, 24, Bitmap.Config.ALPHA_8);
+
+ ArgumentCaptor<CallerInfoLookupHelper.OnQueryCompleteListener> listenerCaptor =
+ ArgumentCaptor.forClass(CallerInfoLookupHelper.OnQueryCompleteListener.class);
+ verify(mMockCallerInfoLookupHelper).startLookup(any(), listenerCaptor.capture());
+ listenerCaptor.getValue().onCallerInfoQueryComplete(call.getHandle(), info);
+
+ assertEquals(info, call.getCallerInfo());
+ assertEquals(info.getName(), call.getName());
+ assertEquals(info.getPhoneNumber(), call.getPhoneNumber());
+ assertEquals(info.cachedPhoto, call.getPhoto());
+ assertEquals(info.cachedPhotoIcon, call.getPhotoIcon());
+ assertEquals(call.getHandle(), call.getContactUri());
+ }
+
+ @Test
+ @SmallTest
+ public void testOriginalCallIntent() {
+ Call call = createCall("1");
+
+ Intent i = new Intent();
+ call.setOriginalCallIntent(i);
+
+ assertEquals(i, call.getOriginalCallIntent());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleCreateConferenceSuccessNotifiesListeners() {
+ Call.Listener listener = mock(Call.Listener.class);
+
+ Call incomingCall = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ incomingCall.setConnectionService(mMockConnectionService);
+ incomingCall.addListener(listener);
+ Call outgoingCall = createCall("2", Call.CALL_DIRECTION_OUTGOING);
+ outgoingCall.setConnectionService(mMockConnectionService);
+ outgoingCall.addListener(listener);
+
+ StatusHints statusHints = mock(StatusHints.class);
+ Bundle extra = new Bundle();
+ ParcelableConference conference =
+ new ParcelableConference.Builder(SIM_1_HANDLE, Connection.STATE_NEW)
+ .setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED)
+ .setConnectionCapabilities(123)
+ .setVideoAttributes(null, VideoProfile.STATE_AUDIO_ONLY)
+ .setRingbackRequested(true)
+ .setStatusHints(statusHints)
+ .setExtras(extra)
+ .build();
+
+ incomingCall.handleCreateConferenceSuccess(null, conference);
+ verify(listener).onSuccessfulIncomingCall(incomingCall);
+
+ outgoingCall.handleCreateConferenceSuccess(null, conference);
+ verify(listener).onSuccessfulOutgoingCall(outgoingCall, CallState.NEW);
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleCreateConferenceSuccess() {
+ Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ call.setConnectionService(mMockConnectionService);
+
+ StatusHints statusHints = mock(StatusHints.class);
+ Bundle extra = new Bundle();
+ ParcelableConference conference =
+ new ParcelableConference.Builder(SIM_1_HANDLE, Connection.STATE_NEW)
+ .setAddress(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED)
+ .setConnectionCapabilities(123)
+ .setVideoAttributes(null, VideoProfile.STATE_AUDIO_ONLY)
+ .setRingbackRequested(true)
+ .setStatusHints(statusHints)
+ .setExtras(extra)
+ .build();
+
+ call.handleCreateConferenceSuccess(null, conference);
+
+ assertEquals(SIM_1_HANDLE, call.getTargetPhoneAccount());
+ assertEquals(TEST_ADDRESS, call.getHandle());
+ assertEquals(123, call.getConnectionCapabilities());
+ assertNull(call.getVideoProviderProxy());
+ assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+ assertTrue(call.isRingbackRequested());
+ assertEquals(statusHints, call.getStatusHints());
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleCreateConferenceFailure() {
+ Call.Listener listener = mock(Call.Listener.class);
+
+ Call incomingCall = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ incomingCall.setConnectionService(mMockConnectionService);
+ incomingCall.addListener(listener);
+ Call outgoingCall = createCall("2", Call.CALL_DIRECTION_OUTGOING);
+ outgoingCall.setConnectionService(mMockConnectionService);
+ outgoingCall.addListener(listener);
+
+ final DisconnectCause cause = new DisconnectCause(DisconnectCause.REJECTED);
+
+ incomingCall.handleCreateConferenceFailure(cause);
+ assertEquals(cause, incomingCall.getDisconnectCause());
+ verify(listener).onFailedIncomingCall(incomingCall);
+
+ outgoingCall.handleCreateConferenceFailure(cause);
+ assertEquals(cause, outgoingCall.getDisconnectCause());
+ verify(listener).onFailedOutgoingCall(outgoingCall, cause);
+ }
+
+ @Test
+ @SmallTest
+ public void testWasConferencePreviouslyMerged() {
+ Call call = createCall("1");
+ call.setConnectionService(mMockConnectionService);
+ call.setConnectionCapabilities(Connection.CAPABILITY_MERGE_CONFERENCE);
+
+ assertFalse(call.wasConferencePreviouslyMerged());
+
+ call.mergeConference();
+
+ assertTrue(call.wasConferencePreviouslyMerged());
+ }
+
+ @Test
+ @SmallTest
+ public void testSwapConference() {
+ Call.Listener listener = mock(Call.Listener.class);
+
+ Call call = createCall("1");
+ call.setConnectionService(mMockConnectionService);
+ call.setConnectionCapabilities(Connection.CAPABILITY_SWAP_CONFERENCE);
+ call.addListener(listener);
+
+ call.swapConference();
+ assertNull(call.getConferenceLevelActiveCall());
+
+ Call childCall1 = createCall("child1");
+ childCall1.setChildOf(call);
+ call.swapConference();
+ assertEquals(childCall1, call.getConferenceLevelActiveCall());
+
+ Call childCall2 = createCall("child2");
+ childCall2.setChildOf(call);
+ call.swapConference();
+ assertEquals(childCall1, call.getConferenceLevelActiveCall());
+ call.swapConference();
+ assertEquals(childCall2, call.getConferenceLevelActiveCall());
+
+ verify(listener, times(4)).onCdmaConferenceSwap(call);
+ }
+
+ @Test
+ @SmallTest
+ public void testHandleCreateConnectionFailure() {
+ Call.Listener listener = mock(Call.Listener.class);
+
+ Call incomingCall = createCall("1", Call.CALL_DIRECTION_INCOMING);
+ incomingCall.setConnectionService(mMockConnectionService);
+ incomingCall.addListener(listener);
+ Call outgoingCall = createCall("2", Call.CALL_DIRECTION_OUTGOING);
+ outgoingCall.setConnectionService(mMockConnectionService);
+ outgoingCall.addListener(listener);
+ Call unknownCall = createCall("3", Call.CALL_DIRECTION_UNKNOWN);
+ unknownCall.setConnectionService(mMockConnectionService);
+ unknownCall.addListener(listener);
+
+ final DisconnectCause cause = new DisconnectCause(DisconnectCause.REJECTED);
+
+ incomingCall.handleCreateConnectionFailure(cause);
+ assertEquals(cause, incomingCall.getDisconnectCause());
+ verify(listener).onFailedIncomingCall(incomingCall);
+
+ outgoingCall.handleCreateConnectionFailure(cause);
+ assertEquals(cause, outgoingCall.getDisconnectCause());
+ verify(listener).onFailedOutgoingCall(outgoingCall, cause);
+
+ unknownCall.handleCreateConnectionFailure(cause);
+ assertEquals(cause, unknownCall.getDisconnectCause());
+ verify(listener).onFailedUnknownCall(unknownCall);
+ }
+
+ /**
+ * ensure a Call object does not throw an NPE when the CallingPackageIdentity is not set and
+ * the correct values are returned when set
+ */
+ @Test
+ @SmallTest
+ public void testCallingPackageIdentity() {
+ final int packageUid = 123;
+ final int packagePid = 1;
+
+ Call call = createCall("1");
+
+ // assert default values for a Calls CallingPackageIdentity are -1 unless set via the setter
+ assertEquals(-1, call.getCallingPackageIdentity().mCallingPackageUid);
+ assertEquals(-1, call.getCallingPackageIdentity().mCallingPackagePid);
+
+ // set the Call objects CallingPackageIdentity via the setter and a bundle
+ Bundle extras = new Bundle();
+ extras.putInt(CallAttributes.CALLER_UID_KEY, packageUid);
+ extras.putInt(CallAttributes.CALLER_PID_KEY, packagePid);
+ // assert that the setter removed the extras
+ assertEquals(packageUid, extras.getInt(CallAttributes.CALLER_UID_KEY));
+ assertEquals(packagePid, extras.getInt(CallAttributes.CALLER_PID_KEY));
+ call.setCallingPackageIdentity(extras);
+ // assert that the setter removed the extras
+ assertEquals(0, extras.getInt(CallAttributes.CALLER_UID_KEY));
+ assertEquals(0, extras.getInt(CallAttributes.CALLER_PID_KEY));
+ // assert the properties are fetched correctly
+ assertEquals(packageUid, call.getCallingPackageIdentity().mCallingPackageUid);
+ assertEquals(packagePid, call.getCallingPackageIdentity().mCallingPackagePid);
+ }
+
+ @Test
+ @SmallTest
+ public void testOnConnectionEventNotifiesListener() {
+ Call.Listener listener = mock(Call.Listener.class);
+ Call call = createCall("1");
+ call.addListener(listener);
+
+ call.onConnectionEvent(Connection.EVENT_ON_HOLD_TONE_START, null);
+ verify(listener).onHoldToneRequested(call);
+ assertTrue(call.isRemotelyHeld());
+
+ call.onConnectionEvent(Connection.EVENT_ON_HOLD_TONE_END, null);
+ verify(listener, times(2)).onHoldToneRequested(call);
+ assertFalse(call.isRemotelyHeld());
+
+ call.onConnectionEvent(Connection.EVENT_CALL_HOLD_FAILED, null);
+ verify(listener).onCallHoldFailed(call);
+
+ call.onConnectionEvent(Connection.EVENT_CALL_SWITCH_FAILED, null);
+ verify(listener).onCallSwitchFailed(call);
+
+ final int d2dType = 1;
+ final int d2dValue = 2;
+ final Bundle d2dExtras = new Bundle();
+ d2dExtras.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE, d2dType);
+ d2dExtras.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE, d2dValue);
+ call.onConnectionEvent(Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE, d2dExtras);
+ verify(listener).onReceivedDeviceToDeviceMessage(call, d2dType, d2dValue);
+
+ final CallQuality quality = new CallQuality();
+ final Bundle callQualityExtras = new Bundle();
+ callQualityExtras.putParcelable(Connection.EXTRA_CALL_QUALITY_REPORT, quality);
+ call.onConnectionEvent(Connection.EVENT_CALL_QUALITY_REPORT, callQualityExtras);
+ verify(listener).onReceivedCallQualityReport(call, quality);
+ }
+
+ @Test
+ @SmallTest
+ public void testDiagnosticMessage() {
+ Call.Listener listener = mock(Call.Listener.class);
+ Call call = createCall("1");
+ call.addListener(listener);
+
+ final int id = 1;
+ final String message = "msg";
+
+ call.displayDiagnosticMessage(id, message);
+ verify(listener).onConnectionEvent(
+ eq(call),
+ eq(android.telecom.Call.EVENT_DISPLAY_DIAGNOSTIC_MESSAGE),
+ argThat(extras -> {
+ return extras.getInt(android.telecom.Call.EXTRA_DIAGNOSTIC_MESSAGE_ID) == id &&
+ extras.getCharSequence(android.telecom.Call.EXTRA_DIAGNOSTIC_MESSAGE)
+ .toString().equals(message);
+ }));
+
+ call.clearDiagnosticMessage(id);
+ verify(listener).onConnectionEvent(
+ eq(call),
+ eq(android.telecom.Call.EVENT_CLEAR_DIAGNOSTIC_MESSAGE),
+ argThat(extras -> {
+ return extras.getInt(android.telecom.Call.EXTRA_DIAGNOSTIC_MESSAGE_ID) == id;
+ }));
+ }
+
+ private Call createCall(String id) {
+ return createCall(id, Call.CALL_DIRECTION_UNDEFINED);
+ }
+
+ private Call createCall(String id, int callDirection) {
+ return new Call(
+ id,
+ mContext,
+ mMockCallsManager,
+ mLock,
+ null,
+ mMockPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null,
+ SIM_1_HANDLE,
+ callDirection,
+ false,
+ false,
+ mMockClockProxy,
+ mMockToastProxy);
}
}
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 160114a67..9f4633667 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.tests;
+import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
+
import static junit.framework.Assert.assertNotNull;
import static junit.framework.TestCase.fail;
@@ -42,17 +44,26 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.Manifest;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.Process;
+import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.BlockedNumberContract;
+import android.telecom.CallException;
+import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -61,18 +72,24 @@ import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
+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.Pair;
import android.widget.Toast;
+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.CallAudioManager;
import com.android.server.telecom.CallAudioModeStateMachine;
import com.android.server.telecom.CallAudioRouteStateMachine;
import com.android.server.telecom.CallDiagnosticServiceController;
+import com.android.server.telecom.CallEndpointController;
+import com.android.server.telecom.CallEndpointControllerFactory;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.CallsManager;
@@ -81,7 +98,9 @@ import com.android.server.telecom.ConnectionServiceFocusManager;
import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
import com.android.server.telecom.ConnectionServiceWrapper;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.EmergencyCallDiagnosticLogger;
import com.android.server.telecom.EmergencyCallHelper;
+import com.android.server.telecom.HandoverState;
import com.android.server.telecom.HeadsetMediaButton;
import com.android.server.telecom.HeadsetMediaButtonFactory;
import com.android.server.telecom.InCallController;
@@ -94,6 +113,7 @@ import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.ProximitySensorManager;
import com.android.server.telecom.ProximitySensorManagerFactory;
+import com.android.server.telecom.Ringer;
import com.android.server.telecom.RoleManagerAdapter;
import com.android.server.telecom.SystemStateHelper;
import com.android.server.telecom.TelecomSystem;
@@ -101,10 +121,13 @@ import com.android.server.telecom.Timeouts;
import com.android.server.telecom.WiredHeadsetManager;
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.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 org.junit.After;
import org.junit.Before;
@@ -120,7 +143,6 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -131,7 +153,10 @@ import java.util.concurrent.TimeUnit;
@RunWith(JUnit4.class)
public class CallsManagerTest extends TelecomTestCase {
private static final int TEST_TIMEOUT = 5000; // milliseconds
+ private static final long STATE_TIMEOUT = 5000L;
private static final int SECONDARY_USER_ID = 12;
+ private static final UserHandle TEST_USER_HANDLE = UserHandle.of(123);
+ private static final String TEST_PACKAGE_NAME = "GoogleMeet";
private static final PhoneAccountHandle SIM_1_HANDLE = new PhoneAccountHandle(
ComponentName.unflattenFromString("com.foo/.Blah"), "Sim1");
private static final PhoneAccountHandle SIM_1_HANDLE_SECONDARY = new PhoneAccountHandle(
@@ -147,12 +172,25 @@ public class CallsManagerTest extends TelecomTestCase {
ComponentName.unflattenFromString("com.voip/.Stuff"), "Voip1");
private static final PhoneAccountHandle SELF_MANAGED_HANDLE = new PhoneAccountHandle(
ComponentName.unflattenFromString("com.baz/.Self"), "Self");
+ private static final PhoneAccountHandle SELF_MANAGED_2_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.baz/.Self2"), "Self2");
+ private static final PhoneAccountHandle WORK_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.foo/.Blah"), "work", new UserHandle(10));
+ private static final PhoneAccountHandle SELF_MANAGED_W_CUSTOM_HANDLE = new PhoneAccountHandle(
+ new ComponentName(TEST_PACKAGE_NAME, "class"), "1", TEST_USER_HANDLE);
private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.Builder(SIM_1_HANDLE, "Sim1")
.setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
| PhoneAccount.CAPABILITY_CALL_PROVIDER
| PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
.setIsEnabled(true)
.build();
+ private static final PhoneAccount SIM_1_ACCOUNT_SECONDARY = new PhoneAccount
+ .Builder(SIM_1_HANDLE_SECONDARY, "Sim1")
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
+ .setIsEnabled(true)
+ .build();
private static final PhoneAccount SIM_2_ACCOUNT = new PhoneAccount.Builder(SIM_2_HANDLE, "Sim2")
.setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
| PhoneAccount.CAPABILITY_CALL_PROVIDER
@@ -164,6 +202,24 @@ public class CallsManagerTest extends TelecomTestCase {
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
.setIsEnabled(true)
.build();
+ private static final PhoneAccount SELF_MANAGED_2_ACCOUNT = new PhoneAccount.Builder(
+ SELF_MANAGED_2_HANDLE, "Self2")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .setIsEnabled(true)
+ .build();
+ private static final PhoneAccount WORK_ACCOUNT = new PhoneAccount.Builder(
+ WORK_HANDLE, "work")
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER
+ | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
+ .setIsEnabled(true)
+ .build();
+ private static final PhoneAccount SM_W_DIFFERENT_PACKAGE_AND_USER = new PhoneAccount.Builder(
+ SELF_MANAGED_W_CUSTOM_HANDLE, "Self")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .setIsEnabled(true)
+ .build();
+
private static final Uri TEST_ADDRESS = Uri.parse("tel:555-1212");
private static final Uri TEST_ADDRESS2 = Uri.parse("tel:555-1213");
private static final Uri TEST_ADDRESS3 = Uri.parse("tel:555-1214");
@@ -171,6 +227,8 @@ public class CallsManagerTest extends TelecomTestCase {
TEST_ADDRESS2, SIM_1_HANDLE,
TEST_ADDRESS3, SIM_2_HANDLE);
+ private static final String DEFAULT_CALL_SCREENING_APP = "com.foo.call_screen_app";
+
private static int sCallId = 1;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
@Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
@@ -199,6 +257,8 @@ public class CallsManagerTest extends TelecomTestCase {
@Mock private AudioProcessingNotification mAudioProcessingNotification;
@Mock private InCallControllerFactory mInCallControllerFactory;
@Mock private InCallController mInCallController;
+ @Mock private CallEndpointControllerFactory mCallEndpointControllerFactory;
+ @Mock private CallEndpointController mCallEndpointController;
@Mock private ConnectionServiceFocusManager mConnectionSvrFocusMgr;
@Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
@Mock private CallAudioRouteStateMachine.Factory mCallAudioRouteStateMachineFactory;
@@ -209,6 +269,14 @@ public class CallsManagerTest extends TelecomTestCase {
@Mock private RoleManagerAdapter mRoleManagerAdapter;
@Mock private ToastFactory mToastFactory;
@Mock private Toast mToast;
+ @Mock private CallAnomalyWatchdog mCallAnomalyWatchdog;
+
+ @Mock private EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
+ @Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
+ @Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
+ @Mock private PhoneCapability mPhoneCapability;
+ @Mock private CallStreamingNotification mCallStreamingNotification;
private CallsManager mCallsManager;
@@ -225,8 +293,10 @@ public class CallsManagerTest extends TelecomTestCase {
mProximitySensorManager);
when(mInCallControllerFactory.create(any(), any(), any(), any(), any(), any(),
any())).thenReturn(mInCallController);
+ when(mCallEndpointControllerFactory.create(any(), any(), any())).thenReturn(
+ mCallEndpointController);
when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(),
- anyInt())).thenReturn(mCallAudioRouteStateMachine);
+ anyInt(), any())).thenReturn(mCallAudioRouteStateMachine);
when(mCallAudioModeStateMachineFactory.create(any(), any()))
.thenReturn(mCallAudioModeStateMachine);
when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
@@ -237,6 +307,9 @@ public class CallsManagerTest extends TelecomTestCase {
.thenReturn(mDisconnectedCallNotifier);
when(mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(any(ContentResolver.class)))
.thenReturn(2000L);
+ when(mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())
+ .thenReturn(STATE_TIMEOUT);
+ when(mClockProxy.elapsedRealtime()).thenReturn(0L);
mCallsManager = new CallsManager(
mComponentContextFixture.getTestDouble().getApplicationContext(),
mLock,
@@ -266,7 +339,16 @@ public class CallsManagerTest extends TelecomTestCase {
mInCallControllerFactory,
mCallDiagnosticServiceController,
mRoleManagerAdapter,
- mToastFactory);
+ mToastFactory,
+ mCallEndpointControllerFactory,
+ mCallAnomalyWatchdog,
+ mAccessibilityManagerAdapter,
+ // Just do async tasks synchronously to support testing.
+ command -> command.run(),
+ mBlockedNumbersAdapter,
+ TransactionManager.getTestInstance(),
+ mEmergencyCallDiagnosticLogger,
+ mCallStreamingNotification);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -274,6 +356,8 @@ public class CallsManagerTest extends TelecomTestCase {
eq(SIM_1_HANDLE), any())).thenReturn(SIM_1_ACCOUNT);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SIM_2_HANDLE), any())).thenReturn(SIM_2_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);
}
@@ -291,18 +375,9 @@ public class CallsManagerTest extends TelecomTestCase {
assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null, false, false).size());
}
- /**
- * Verify behavior for multisim devices where we want to ensure that the active sim is used for
- * placing a new call.
- * @throws Exception
- */
- @MediumTest
- @Test
- public void testConstructPossiblePhoneAccountsMultiSimActive() throws Exception {
- setupMsimAccounts();
-
+ private Call constructOngoingCall(String callId, PhoneAccountHandle phoneAccountHandle) {
Call ongoingCall = new Call(
- "1", /* callId */
+ callId,
mContext,
mCallsManager,
mLock,
@@ -311,13 +386,26 @@ public class CallsManagerTest extends TelecomTestCase {
TEST_ADDRESS,
null /* GatewayInfo */,
null /* connectionManagerPhoneAccountHandle */,
- SIM_2_HANDLE,
+ phoneAccountHandle,
Call.CALL_DIRECTION_INCOMING,
false /* shouldAttachToExistingConnection*/,
false /* isConference */,
mClockProxy,
mToastFactory);
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.
+ * @throws Exception
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive() throws Exception {
+ setupMsimAccounts();
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
mCallsManager.addCall(ongoingCall);
List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
@@ -340,6 +428,47 @@ public class CallsManagerTest extends TelecomTestCase {
assertEquals(2, 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
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible() throws
+ Exception {
+ setupMsimAccounts();
+ setMaxActiveVoiceSubscriptions(2);
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, false);
+ assertEquals(2, 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
+ */
+ @MediumTest
+ @Test
+ public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible_emergencyCall()
+ throws Exception {
+ setupMsimAccounts();
+ setMaxActiveVoiceSubscriptions(2);
+
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+ mCallsManager.addCall(ongoingCall);
+
+ List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+ TEST_ADDRESS, null, false, true /* isEmergency */);
+ assertEquals(1, phoneAccountHandles.size());
+ assertEquals(SIM_2_HANDLE, phoneAccountHandles.get(0));
+ }
+
private void setupCallerInfoLookupHelper() {
doAnswer(invocation -> {
Uri handle = invocation.getArgument(0);
@@ -383,7 +512,7 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
SIM_1_HANDLE);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
@@ -407,7 +536,7 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
null);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
@@ -431,8 +560,8 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
null);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), eq(PhoneAccount.CAPABILITY_VIDEO_CALLING), anyInt())).thenReturn(
- new ArrayList<>(Arrays.asList(SIM_2_HANDLE)));
+ any(), eq(PhoneAccount.CAPABILITY_VIDEO_CALLING), anyInt(), anyBoolean()))
+ .thenReturn(new ArrayList<>(Arrays.asList(SIM_2_HANDLE)));
List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
null /* phoneAcct */, TEST_ADDRESS, true /* isVideo */, false /* isEmergency */, null /* userHandle */)
@@ -455,11 +584,11 @@ public class CallsManagerTest extends TelecomTestCase {
null);
// When querying for video capable accounts, return nothing.
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), eq(PhoneAccount.CAPABILITY_VIDEO_CALLING), anyInt())).thenReturn(
- Collections.emptyList());
+ any(), eq(PhoneAccount.CAPABILITY_VIDEO_CALLING), anyInt(), anyBoolean())).
+ thenReturn(Collections.emptyList());
// When querying for non-video capable accounts, return one.
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), eq(0 /* none specified */), anyInt())).thenReturn(
+ any(), eq(0 /* none specified */), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE)));
List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
null /* phoneAcct */, TEST_ADDRESS, true /* isVideo */, false /* isEmergency */, null /* userHandle */)
@@ -481,7 +610,7 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
null);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
@@ -502,7 +631,7 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
null);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
List<PhoneAccountHandle> accounts = mCallsManager.findOutgoingCallPhoneAccount(
@@ -597,6 +726,54 @@ public class CallsManagerTest extends TelecomTestCase {
verify(heldCall).unhold(any());
}
+ /**
+ * Ensures we don't auto-unhold a call from a different app when we locally disconnect a call.
+ */
+ @SmallTest
+ @Test
+ public void testDontUnholdCallsBetweenConnectionServices() {
+ // GIVEN a CallsManager with ongoing call
+ Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(ongoingCall.isDisconnectHandledViaFuture()).thenReturn(false);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_HOLD);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ // and a held call which has different ConnectionService
+ Call heldCall = addSpyCall(VOIP_1_HANDLE, CallState.ON_HOLD);
+
+ // Disconnect and cleanup the active ongoing call.
+ mCallsManager.disconnectCall(ongoingCall);
+ mCallsManager.markCallAsRemoved(ongoingCall);
+
+ // Should not unhold the held call since its in another app.
+ verify(heldCall, never()).unhold();
+ }
+
+ /**
+ * Ensures we do auto-unhold a call from the same app when we locally disconnect a call.
+ */
+ @SmallTest
+ @Test
+ public void testUnholdCallWhenDisconnectingInSameApp() {
+ // GIVEN a CallsManager with ongoing call
+ Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(ongoingCall.isDisconnectHandledViaFuture()).thenReturn(false);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_HOLD);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ // and a held call which has same ConnectionService
+ Call heldCall = addSpyCall(SIM_1_HANDLE, CallState.ON_HOLD);
+
+ // Disconnect and cleanup the active ongoing call.
+ mCallsManager.disconnectCall(ongoingCall);
+ mCallsManager.markCallAsRemoved(ongoingCall);
+
+ // Should auto-unhold the held call since its in the same app.
+ verify(heldCall).unhold();
+ }
+
@SmallTest
@Test
public void testUnholdCallWhenOngoingEmergCallCanNotBeHeldAndFromDifferentConnectionService() {
@@ -675,7 +852,7 @@ public class CallsManagerTest extends TelecomTestCase {
mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
// THEN the ongoing call is held and the focus request for incoming call is sent
- verify(ongoingCall).hold();
+ verify(ongoingCall).hold(anyString());
verifyFocusRequestAndExecuteCallback(incomingCall);
// and the incoming call is answered.
@@ -785,6 +962,64 @@ public class CallsManagerTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testAnswerThirdCallWhenTwoCallsOnDifferentSims_disconnectsHeldCall() {
+ // Given an ongoing call on SIM1
+ Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(CallState.ACTIVE).when(ongoingCall).getState();
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ // And a held call on SIM2, which belongs to the same ConnectionService
+ Call heldCall = addSpyCall(SIM_2_HANDLE, CallState.ON_HOLD);
+ doReturn(CallState.ON_HOLD).when(heldCall).getState();
+
+ // on answering an incoming call on SIM1, which belongs to the same ConnectionService
+ Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.RINGING);
+ mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
+
+ // THEN the previous held call is disconnected
+ verify(heldCall).disconnect();
+
+ // and the ongoing call is held
+ verify(ongoingCall).hold();
+
+ // and the focus request is sent
+ verifyFocusRequestAndExecuteCallback(incomingCall);
+ // and the incoming call is answered
+ verify(incomingCall).answer(VideoProfile.STATE_AUDIO_ONLY);
+ }
+
+ @SmallTest
+ @Test
+ public void testAnswerThirdCallDifferentSimWhenTwoCallsOnSameSim_disconnectsHeldCall() {
+ // Given an ongoing call on SIM1
+ Call ongoingCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ doReturn(true).when(ongoingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(CallState.ACTIVE).when(ongoingCall).getState();
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ // And a held call on SIM1
+ Call heldCall = addSpyCall(SIM_1_HANDLE, CallState.ON_HOLD);
+ doReturn(CallState.ON_HOLD).when(heldCall).getState();
+
+ // on answering an incoming call on SIM2, which belongs to the same ConnectionService
+ Call incomingCall = addSpyCall(SIM_2_HANDLE, CallState.RINGING);
+ mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
+
+ // THEN the previous held call is disconnected
+ verify(heldCall).disconnect();
+
+ // and the ongoing call is held
+ verify(ongoingCall).hold();
+
+ // and the focus request is sent
+ verifyFocusRequestAndExecuteCallback(incomingCall);
+ // and the incoming call is answered
+ verify(incomingCall).answer(VideoProfile.STATE_AUDIO_ONLY);
+ }
+
+ @SmallTest
+ @Test
public void testAnswerCallWhenNoOngoingCallExisted() {
// GIVEN a CallsManager with no ongoing call.
@@ -883,7 +1118,7 @@ public class CallsManagerTest extends TelecomTestCase {
mCallsManager.markCallAsActive(newCall);
// THEN the ongoing call is held
- verify(ongoingCall).hold();
+ verify(ongoingCall).hold(anyString());
verifyFocusRequestAndExecuteCallback(newCall);
// and the new call is active
@@ -942,6 +1177,43 @@ public class CallsManagerTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testNoFilteringOfNetworkIdentifiedEmergencyCalls() {
+ // GIVEN an incoming call which is network identified as an emergency call.
+ Call incomingCall = addSpyCall(CallState.NEW);
+ incomingCall.setConnectionProperties(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(true).when(incomingCall)
+ .hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL);
+ doReturn(true).when(incomingCall).setState(anyInt(), any());
+
+ // WHEN the incoming call is successfully added.
+ mCallsManager.onSuccessfulIncomingCall(incomingCall);
+
+ // THEN the incoming call is not using call filtering
+ verify(incomingCall).setIsUsingCallFiltering(eq(false));
+ }
+
+ @SmallTest
+ @Test
+ public void testNoFilteringOfEmergencySmsModeCalls() {
+ // GIVEN an incoming call which is network identified as an emergency call.
+ Call incomingCall = addSpyCall(CallState.NEW);
+ when(mComponentContextFixture.getTelephonyManager().isInEmergencySmsMode())
+ .thenReturn(true);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(true).when(incomingCall).setState(anyInt(), any());
+
+ // WHEN the incoming call is successfully added.
+ mCallsManager.onSuccessfulIncomingCall(incomingCall);
+
+ // THEN the incoming call is not using call filtering
+ verify(incomingCall).setIsUsingCallFiltering(eq(false));
+ }
+
+ @SmallTest
+ @Test
public void testAcceptIncomingCallWhenHeadsetMediaButtonShortPress() {
// GIVEN an incoming call
Call incomingCall = addSpyCall();
@@ -1141,7 +1413,7 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
SIM_1_HANDLE);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
SIM_2_HANDLE.getUserHandle(), service);
@@ -1169,13 +1441,132 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE))
.thenReturn(SIM_1_ACCOUNT);
- assertFalse(mCallsManager.isIncomingCallPermitted(null, SELF_MANAGED_HANDLE));
- assertFalse(mCallsManager.isIncomingCallPermitted(null, SIM_1_HANDLE));
+ assertFalse(mCallsManager.isIncomingCallPermitted(SELF_MANAGED_HANDLE));
+ assertFalse(mCallsManager.isIncomingCallPermitted(SIM_1_HANDLE));
+ }
+
+ @MediumTest
+ @Test
+ public void testManagedIncomingCallPermitted() {
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE))
+ .thenReturn(SIM_1_ACCOUNT);
+
+ // Don't care
+ Call selfManagedCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.ACTIVE);
+ when(selfManagedCall.isSelfManaged()).thenReturn(true);
+ assertTrue(mCallsManager.isIncomingCallPermitted(SIM_1_HANDLE));
+
+ Call existingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(existingCall.isSelfManaged()).thenReturn(false);
+
+ when(existingCall.getState()).thenReturn(CallState.RINGING);
+ assertFalse(mCallsManager.isIncomingCallPermitted(SIM_1_HANDLE));
+
+ when(existingCall.getState()).thenReturn(CallState.ON_HOLD);
+ assertFalse(mCallsManager.isIncomingCallPermitted(SIM_1_HANDLE));
+ }
+
+ @MediumTest
+ @Test
+ public void testSelfManagedIncomingCallPermitted() {
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SELF_MANAGED_HANDLE))
+ .thenReturn(SELF_MANAGED_ACCOUNT);
+
+ // Don't care
+ Call managedCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(managedCall.isSelfManaged()).thenReturn(false);
+ assertTrue(mCallsManager.isIncomingCallPermitted(SELF_MANAGED_HANDLE));
+
+ Call existingCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.RINGING);
+ when(existingCall.isSelfManaged()).thenReturn(true);
+ assertFalse(mCallsManager.isIncomingCallPermitted(SELF_MANAGED_HANDLE));
+
+ when(existingCall.getState()).thenReturn(CallState.ACTIVE);
+ assertTrue(mCallsManager.isIncomingCallPermitted(SELF_MANAGED_HANDLE));
+
+ // Add self managed calls up to 10
+ for (int i = 0; i < 9; i++) {
+ Call selfManagedCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.ON_HOLD);
+ when(selfManagedCall.isSelfManaged()).thenReturn(true);
+ }
+ assertFalse(mCallsManager.isIncomingCallPermitted(SELF_MANAGED_HANDLE));
}
@SmallTest
@Test
- public void testMakeRoomForOutgoingCallAudioProcessingInProgress() {
+ public void testManagedOutgoingCallPermitted() {
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE))
+ .thenReturn(SIM_1_ACCOUNT);
+
+ // Don't care
+ Call selfManagedCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.ACTIVE);
+ when(selfManagedCall.isSelfManaged()).thenReturn(true);
+ assertTrue(mCallsManager.isOutgoingCallPermitted(SIM_1_HANDLE));
+
+ Call existingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(existingCall.isSelfManaged()).thenReturn(false);
+
+ when(existingCall.getState()).thenReturn(CallState.CONNECTING);
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SIM_1_HANDLE));
+
+ when(existingCall.getState()).thenReturn(CallState.DIALING);
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SIM_1_HANDLE));
+
+ when(existingCall.getState()).thenReturn(CallState.ACTIVE);
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SIM_1_HANDLE));
+
+ when(existingCall.getState()).thenReturn(CallState.ON_HOLD);
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SIM_1_HANDLE));
+ }
+
+ @SmallTest
+ @Test
+ public void testSelfManagedOutgoingCallPermitted() {
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SELF_MANAGED_HANDLE))
+ .thenReturn(SELF_MANAGED_ACCOUNT);
+
+ // Don't care
+ Call managedCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(managedCall.isSelfManaged()).thenReturn(false);
+ assertTrue(mCallsManager.isOutgoingCallPermitted(SELF_MANAGED_HANDLE));
+
+ Call ongoingCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.ACTIVE);
+ when(ongoingCall.isSelfManaged()).thenReturn(true);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(ongoingCall);
+
+ when(ongoingCall.can(Connection.CAPABILITY_HOLD)).thenReturn(false);
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SELF_MANAGED_HANDLE));
+
+ when(ongoingCall.can(Connection.CAPABILITY_HOLD)).thenReturn(true);
+ assertTrue(mCallsManager.isOutgoingCallPermitted(SELF_MANAGED_HANDLE));
+
+ Call handoverCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.NEW);
+ when(handoverCall.isSelfManaged()).thenReturn(true);
+ when(handoverCall.getHandoverSourceCall()).thenReturn(mock(Call.class));
+ assertTrue(mCallsManager.isOutgoingCallPermitted(handoverCall, SELF_MANAGED_HANDLE));
+
+ // Add self managed calls up to 10
+ for (int i = 0; i < 8; i++) {
+ Call selfManagedCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.ON_HOLD);
+ when(selfManagedCall.isSelfManaged()).thenReturn(true);
+ }
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SELF_MANAGED_HANDLE));
+ }
+
+ @SmallTest
+ @Test
+ public void testSelfManagedOutgoingCallPermittedHasEmergencyCall() {
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SELF_MANAGED_HANDLE))
+ .thenReturn(SELF_MANAGED_ACCOUNT);
+
+ Call emergencyCall = addSpyCall();
+ when(emergencyCall.isEmergencyCall()).thenReturn(true);
+ assertFalse(mCallsManager.isOutgoingCallPermitted(SELF_MANAGED_HANDLE));
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForEmergencyCallAudioProcessingInProgress() {
Call ongoingCall = addSpyCall(SIM_2_HANDLE, CallState.AUDIO_PROCESSING);
Call newEmergencyCall = createCall(SIM_1_HANDLE, CallState.NEW);
@@ -1190,7 +1581,7 @@ public class CallsManagerTest extends TelecomTestCase {
@SmallTest
@Test
- public void testMakeRoomForEmergencyDuringIncomingCall() {
+ public void testMakeRoomForEmergencyCallDuringIncomingCall() {
Call ongoingCall = addSpyCall(SIM_2_HANDLE, CallState.RINGING);
Call newEmergencyCall = createCall(SIM_1_HANDLE, CallState.NEW);
@@ -1253,21 +1644,230 @@ public class CallsManagerTest extends TelecomTestCase {
verify(ringingCall).reject(anyBoolean(), any(), any());
}
+ /**
+ * Verifies that an anomaly report is triggered when a stuck/zombie call is found and force
+ * disconnected when making room for an outgoing call.
+ */
@SmallTest
@Test
- public void testMakeRoomForOutgoingCallConnecting() {
+ public void testAnomalyReportedWhenMakeRoomForOutgoingCallConnecting() {
+ mCallsManager.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
Call ongoingCall = addSpyCall(SIM_2_HANDLE, CallState.CONNECTING);
Call newCall = createCall(SIM_1_HANDLE, CallState.NEW);
when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
.thenReturn(false);
- newCall.setHandle(Uri.fromParts("tel", "5551213", null),
- TelecomManager.PRESENTATION_ALLOWED);
+ newCall.setHandle(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+ // Make sure enough time has passed that we'd drop the connecting call.
+ when(mClockProxy.elapsedRealtime()).thenReturn(STATE_TIMEOUT + 10L);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newCall));
+ verify(mAnomalyReporterAdapter).reportAnomaly(
+ CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+ CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+ verify(ongoingCall).disconnect(anyLong(), anyString());
+ }
+
+ /**
+ * Verifies that we won't auto-disconnect an outgoing CONNECTING call unless it has timed out.
+ */
+ @SmallTest
+ @Test
+ public void testDontDisconnectConnectingCallWhenNotTimedOut() {
+ mCallsManager.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
+ Call ongoingCall = addSpyCall(SIM_2_HANDLE, CallState.CONNECTING);
+
+ Call newCall = createCall(SIM_1_HANDLE, CallState.NEW);
+ when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+ .thenReturn(false);
+ newCall.setHandle(TEST_ADDRESS, TelecomManager.PRESENTATION_ALLOWED);
+
+ // Make sure it has been a short time so we don't try to disconnect the call
+ when(mClockProxy.elapsedRealtime()).thenReturn(STATE_TIMEOUT / 2);
+ assertFalse(mCallsManager.makeRoomForOutgoingCall(newCall));
+ verify(ongoingCall, never()).disconnect(anyLong(), anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForEmergencyCallHasOutgoingCall() {
+ Call outgoingCall = addSpyCall(SIM_1_HANDLE, CallState.CONNECTING);
+ when(outgoingCall.isEmergencyCall()).thenReturn(false);
+
+ Call newEmergencyCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(newEmergencyCall.isEmergencyCall()).thenReturn(true);
+
+ assertTrue(mCallsManager.makeRoomForOutgoingEmergencyCall(newEmergencyCall));
+ verify(outgoingCall).disconnect(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForEmergencyCallHasOutgoingEmergencyCall() {
+ Call outgoingCall = addSpyCall(SIM_1_HANDLE, CallState.CONNECTING);
+ when(outgoingCall.isEmergencyCall()).thenReturn(true);
+
+ Call newEmergencyCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(newEmergencyCall.isEmergencyCall()).thenReturn(true);
+
+ assertFalse(mCallsManager.makeRoomForOutgoingEmergencyCall(newEmergencyCall));
+ verify(outgoingCall, never()).disconnect(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForEmergencyCallHasUnholdableCallAndManagedCallInHold() {
+ Call unholdableCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(unholdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(false);
+ Call managedHoldingCall = addSpyCall(SIM_1_HANDLE, CallState.ON_HOLD);
+ when(managedHoldingCall.isSelfManaged()).thenReturn(false);
+
+ Call newEmergencyCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(newEmergencyCall.isEmergencyCall()).thenReturn(true);
+
+ assertTrue(mCallsManager.makeRoomForOutgoingEmergencyCall(newEmergencyCall));
+ verify(unholdableCall).disconnect(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForEmergencyCallHasHoldableCall() {
+ Call holdableCall = addSpyCall(null, CallState.ACTIVE);
+ when(holdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(true);
+
+ Call newEmergencyCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(newEmergencyCall.isEmergencyCall()).thenReturn(true);
+
+ assertTrue(mCallsManager.makeRoomForOutgoingEmergencyCall(newEmergencyCall));
+ verify(holdableCall).hold(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForEmergencyCallHasUnholdableCall() {
+ Call unholdableCall = addSpyCall(null, CallState.ACTIVE);
+ when(unholdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(false);
+
+ Call newEmergencyCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+ when(newEmergencyCall.isEmergencyCall()).thenReturn(true);
+
+ assertFalse(mCallsManager.makeRoomForOutgoingEmergencyCall(newEmergencyCall));
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallHasConnectingCall() {
+ Call ongoingCall = addSpyCall(SIM_2_HANDLE, CallState.CONNECTING);
+ Call newCall = createCall(SIM_1_HANDLE, CallState.NEW);
+
+ when(mClockProxy.elapsedRealtime()).thenReturn(STATE_TIMEOUT + 10L);
assertTrue(mCallsManager.makeRoomForOutgoingCall(newCall));
verify(ongoingCall).disconnect(anyLong(), anyString());
}
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameCall() {
+ addSpyCall(SIM_2_HANDLE, CallState.CONNECTING);
+ Call ongoingCall2 = addSpyCall();
+ when(mClockProxy.elapsedRealtime()).thenReturn(STATE_TIMEOUT + 10L);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(ongoingCall2));
+ }
+
+ /**
+ * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+ * active call. This assumes same connection service in the same app.
+ */
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameVoipApp() {
+ Call activeCall = addSpyCall(SELF_MANAGED_HANDLE, null /* connMgr */,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0 /* properties */);
+ Call newDialingCall = createCall(SELF_MANAGED_HANDLE, CallState.DIALING);
+ newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+ verify(activeCall).hold(anyString());
+ }
+
+ /**
+ * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+ * active call. This assumes different connection services in the same app.
+ */
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameVoipAppDifferentConnectionService() {
+ Call activeCall = addSpyCall(SELF_MANAGED_HANDLE, null /* connMgr */,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0 /* properties */);
+ Call newDialingCall = createCall(SELF_MANAGED_2_HANDLE, CallState.DIALING);
+ newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+ verify(activeCall).hold(anyString());
+ }
+
+ /**
+ * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+ * active call. This assumes different connection services in the same app.
+ */
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallForSameNonVoipApp() {
+ Call activeCall = addSpyCall(SIM_1_HANDLE, null /* connMgr */,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+ 0 /* properties */);
+ Call newDialingCall = createCall(SIM_1_HANDLE, CallState.DIALING);
+ newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_SUPPORT_HOLD);
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+ verify(activeCall, never()).hold(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallHasOutgoingCallSelectingAccount() {
+ Call outgoingCall = addSpyCall(SIM_1_HANDLE, CallState.SELECT_PHONE_ACCOUNT);
+ Call newCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newCall));
+ verify(outgoingCall).disconnect(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallHasDialingCall() {
+ addSpyCall(SIM_1_HANDLE, CallState.DIALING);
+ Call newCall = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+
+ assertFalse(mCallsManager.makeRoomForOutgoingCall(newCall));
+ }
+
+ @MediumTest
+ @Test
+ public void testMakeRoomForOutgoingCallHasHoldableCall() {
+ Call holdableCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(holdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(true);
+
+ Call newCall = createSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+
+ assertTrue(mCallsManager.makeRoomForOutgoingCall(newCall));
+ verify(holdableCall).hold(anyString());
+ }
+
+ @SmallTest
+ @Test
+ public void testMakeRoomForOutgoingCallHasUnholdableCall() {
+ Call holdableCall = addSpyCall(SIM_1_HANDLE, CallState.ACTIVE);
+ when(holdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(false);
+
+ Call newCall = createSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+
+ assertFalse(mCallsManager.makeRoomForOutgoingCall(newCall));
+ }
+
/**
* Verifies that changes to a {@link PhoneAccount}'s
* {@link PhoneAccount#CAPABILITY_VIDEO_CALLING} capability will be reflected on a call.
@@ -1399,6 +1999,7 @@ public class CallsManagerTest extends TelecomTestCase {
Call screenedCall = mock(Call.class);
Bundle extra = new Bundle();
when(screenedCall.getIntentExtras()).thenReturn(extra);
+ when(screenedCall.getTargetPhoneAccount()).thenReturn(SIM_1_HANDLE);
String appName = "blah";
CallFilteringResult result = new CallFilteringResult.Builder()
.setShouldAllowCall(true)
@@ -1477,7 +2078,7 @@ public class CallsManagerTest extends TelecomTestCase {
when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
SIM_1_HANDLE);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
// Let's add an existing call which is in connecting state; this emulates the case where
@@ -1604,7 +2205,35 @@ public class CallsManagerTest extends TelecomTestCase {
any(DisconnectCause.class));
verify(callSpy, never()).setDisconnectCause(any(DisconnectCause.class));
}
-
+
+ @SmallTest
+ @Test
+ public void testCallStreamingStateChanged() throws Exception {
+ Call call = createCall(SIM_1_HANDLE, CallState.NEW);
+ call.setIsTransactionalCall(true);
+ CountDownLatch streamingStarted = new CountDownLatch(1);
+ CountDownLatch streamingStopped = new CountDownLatch(1);
+ Call.Listener l = new Call.ListenerBase() {
+ @Override
+ public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+ if (isStreaming) {
+ streamingStarted.countDown();
+ } else {
+ streamingStopped.countDown();
+ }
+ }
+ };
+ call.addListener(l);
+
+ // Start call streaming
+ call.startStreaming();
+ assertTrue(streamingStarted.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+
+ // Stop call streaming
+ call.stopStreaming();
+ assertTrue(streamingStopped.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
/**
* Verifies that if call state goes from DIALING to DISCONNECTED, and a call diagnostic service
* IS in use, it would call onCallDisconnected of the CallDiagnosticService
@@ -1645,6 +2274,79 @@ public class CallsManagerTest extends TelecomTestCase {
SELF_MANAGED_HANDLE.getUserHandle()));
}
+ /**
+ * Emulate the case where a new incoming call is created but the connection fails for a known
+ * reason before being added to CallsManager. In this case, the listeners should be notified
+ * properly.
+ */
+ @Test
+ public void testIncomingCallCreatedButNotAddedNotifyListener() {
+ //The call is created and a listener is added:
+ Call incomingCall = createCall(SIM_2_HANDLE, null, CallState.NEW);
+ CallsManager.CallsManagerListener listener = mock(CallsManager.CallsManagerListener.class);
+ mCallsManager.addListener(listener);
+
+ //The connection fails before being added to CallsManager for a known reason:
+ incomingCall.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
+
+ //Ensure the listener is notified properly:
+ verify(listener).onCreateConnectionFailed(incomingCall);
+ }
+
+ /**
+ * Emulate the case where a new incoming call is created but the connection fails for a known
+ * reason after being added to CallsManager. Since the call was added to CallsManager, the
+ * listeners should not be notified via onCreateConnectionFailed().
+ */
+ @Test
+ public void testIncomingCallCreatedAndAddedDoNotNotifyListener() {
+ //The call is created and a listener is added:
+ Call incomingCall = createCall(SIM_2_HANDLE, null, CallState.NEW);
+ CallsManager.CallsManagerListener listener = mock(CallsManager.CallsManagerListener.class);
+ mCallsManager.addListener(listener);
+
+ //The call is added to CallsManager:
+ mCallsManager.addCall(incomingCall);
+
+ //The connection fails after being added to CallsManager for a known reason:
+ incomingCall.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
+
+ //Since the call was added to CallsManager, onCreateConnectionFailed shouldn't be invoked:
+ verify(listener, never()).onCreateConnectionFailed(incomingCall);
+ }
+
+ /**
+ * Emulate the case where a new outgoing call is created but is aborted before being added to
+ * CallsManager since there are no available phone accounts. In this case, the listeners
+ * should be notified properly.
+ */
+ @Test
+ public void testAbortOutgoingCallNoPhoneAccountsNotifyListeners() throws Exception {
+ // Setup a new outgoing call and add a listener
+ Call newCall = addSpyCall(CallState.NEW);
+ CallsManager.CallsManagerListener listener = mock(CallsManager.CallsManagerListener.class);
+ mCallsManager.addListener(listener);
+
+ // Ensure contact info lookup succeeds but do not set the phone account info
+ doAnswer(invocation -> {
+ Uri handle = invocation.getArgument(0);
+ CallerInfo info = new CallerInfo();
+ CompletableFuture<Pair<Uri, CallerInfo>> callerInfoFuture = new CompletableFuture<>();
+ callerInfoFuture.complete(new Pair<>(handle, info));
+ return callerInfoFuture;
+ }).when(mCallerInfoLookupHelper).startLookup(any(Uri.class));
+
+ // Start the outgoing call
+ CompletableFuture<Call> callFuture = mCallsManager.startOutgoingCall(
+ newCall.getHandle(), newCall.getTargetPhoneAccount(), new Bundle(),
+ UserHandle.CURRENT, new Intent(), "com.test.stuff");
+ Call result = callFuture.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ //Ensure the listener is notified properly:
+ verify(listener).onCreateConnectionFailed(any());
+ assertNull(result);
+ }
+
@Test
public void testIsInSelfManagedCallOnlySelfManaged() {
Call selfManagedCall = createCall(SELF_MANAGED_HANDLE, CallState.ACTIVE);
@@ -1676,6 +2378,43 @@ public class CallsManagerTest extends TelecomTestCase {
new UserHandle(90210)));
}
+ /**
+ * Verifies that if a {@link android.telecom.CallScreeningService} app can properly request
+ * notification show for rejected calls.
+ */
+ @SmallTest
+ @Test
+ public void testCallScreeningServiceRequestShowNotification() {
+ Call callSpy = addSpyCall(CallState.NEW);
+ CallFilteringResult result = new CallFilteringResult.Builder()
+ .setShouldAllowCall(false)
+ .setShouldReject(true)
+ .setCallScreeningComponentName("com.foo/.Blah")
+ .setCallScreeningAppName("Blah")
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true).build();
+
+ mCallsManager.onCallFilteringComplete(callSpy, result, false /* timeout */);
+ verify(mMissedCallNotifier).showMissedCallNotification(
+ any(MissedCallNotifier.CallInfo.class));
+ }
+
+ @Test
+ public void testSetStateOnlyCalledOnce() {
+ // GIVEN a new self-managed call
+ Call newCall = addSpyCall();
+ doReturn(true).when(newCall).isSelfManaged();
+ newCall.setState(CallState.DISCONNECTED, "");
+
+ // WHEN ActionSetCallState is given a disconnect call
+ assertEquals(CallState.DISCONNECTED, newCall.getState());
+ // attempt to set the call active
+ mCallsManager.createActionSetCallStateAndPerformAction(newCall, CallState.ACTIVE, "");
+
+ // THEN assert remains disconnected
+ assertEquals(CallState.DISCONNECTED, newCall.getState());
+ }
+
@SmallTest
@Test
public void testCrossUserCallRedirectionEndEarlyForIncapablePhoneAccount() {
@@ -1693,6 +2432,825 @@ public class CallsManagerTest extends TelecomTestCase {
assertTrue(argumentCaptor.getValue().contains("Unavailable phoneAccountHandle"));
}
+ /**
+ * 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.
+ * @throws Exception
+ */
+ @Test
+ public void testStartOutgoingCall_TargetPhoneAccountSet() throws Exception {
+ // Ensure contact info lookup succeeds
+ doAnswer(invocation -> {
+ Uri handle = invocation.getArgument(0);
+ CallerInfo info = new CallerInfo();
+ CompletableFuture<Pair<Uri, CallerInfo>> callerInfoFuture = new CompletableFuture<>();
+ callerInfoFuture.complete(new Pair<>(handle, info));
+ return callerInfoFuture;
+ }).when(mCallerInfoLookupHelper).startLookup(any(Uri.class));
+
+ // Ensure we have candidate phone account handle info.
+ when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
+ SIM_1_HANDLE);
+ when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
+ new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+
+ // start an outgoing call
+ CompletableFuture<Call> callFuture = mCallsManager.startOutgoingCall(
+ TEST_ADDRESS, SIM_2_HANDLE, new Bundle(),
+ UserHandle.CURRENT, new Intent(), "com.test.stuff");
+ Call outgoingCall = callFuture.get();
+ // assert call was created
+ assertNotNull(outgoingCall);
+ // assert target phone account was set
+ assertNotNull(outgoingCall.getTargetPhoneAccount());
+ }
+
+ /**
+ * Verifies that target phone account is set before call filtering occurs.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testOnSuccessfulIncomingCall_TargetPhoneAccountSet() throws Exception {
+ Call incomingCall = addSpyCall(CallState.NEW);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD);
+ doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+ doReturn(true).when(incomingCall).isSelfManaged();
+ doReturn(true).when(incomingCall).setState(anyInt(), any());
+ // assert phone account is present before onSuccessfulIncomingCall is called
+ assertNotNull(incomingCall.getTargetPhoneAccount());
+ }
+
+ /**
+ * Verifies that outgoing call's post call package name is set during
+ * onSuccessfulOutgoingCall.
+ * @throws Exception
+ */
+ @SmallTest
+ @Test
+ public void testPostCallPackageNameSetOnSuccessfulOutgoingCall() throws Exception {
+ Call outgoingCall = addSpyCall(CallState.NEW);
+ when(mCallsManager.getRoleManagerAdapter().getDefaultCallScreeningApp(
+ outgoingCall.getAssociatedUser()))
+ .thenReturn(DEFAULT_CALL_SCREENING_APP);
+ assertNull(outgoingCall.getPostCallPackageName());
+ mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.CONNECTING);
+ assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName());
+ }
+
+ @SmallTest
+ @Test
+ public void testRejectIncomingCallOnPAHInactive_SecondaryUser() throws Exception {
+ ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+ doReturn(WORK_HANDLE.getComponentName()).when(service).getComponentName();
+ 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(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
+ .thenReturn(WORK_ACCOUNT);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ WORK_HANDLE, new Bundle(), false);
+
+ verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any());
+ assertFalse(newCall.isInECBM());
+ assertEquals(USER_MISSED_NOT_RUNNING, newCall.getMissedReason());
+ }
+
+ @SmallTest
+ @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);
+
+ UserManager um = mContext.getSystemService(UserManager.class);
+ when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ SIM_2_HANDLE, new Bundle(), false);
+
+ verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any());
+ assertFalse(newCall.isInECBM());
+ assertEquals(USER_MISSED_NOT_RUNNING, newCall.getMissedReason());
+ }
+
+ @SmallTest
+ @Test
+ public void testAcceptIncomingCallOnPAHInactiveAndECBMActive() 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);
+
+ 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);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ SIM_2_HANDLE, new Bundle(), false);
+
+ assertTrue(newCall.isInECBM());
+ verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+ }
+
+ @SmallTest
+ @Test
+ public void testAcceptIncomingCallOnPAHInactiveAndECBMActive_SecondaryUser() throws Exception {
+ ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+ doReturn(WORK_HANDLE.getComponentName()).when(service).getComponentName();
+ mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+ WORK_HANDLE.getUserHandle(), service);
+
+ 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(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
+ .thenReturn(WORK_ACCOUNT);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ WORK_HANDLE, new Bundle(), false);
+
+ assertTrue(newCall.isInECBM());
+ verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+ }
+
+ @SmallTest
+ @Test
+ public void testAcceptIncomingEmergencyCallOnPAHInactive() 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);
+
+ 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(tm.isEmergencyNumber(any(String.class))).thenReturn(true);
+ Call newCall = mCallsManager.processIncomingCallIntent(
+ SIM_2_HANDLE, extras, false);
+
+ assertFalse(newCall.isInECBM());
+ assertTrue(newCall.isEmergencyCall());
+ verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+ }
+
+ public class LatchedOutcomeReceiver implements OutcomeReceiver<Boolean,
+ CallException> {
+ CountDownLatch mCountDownLatch;
+ Boolean mIsOnResultExpected;
+
+ public LatchedOutcomeReceiver(CountDownLatch latch, boolean isOnResultExpected){
+ mCountDownLatch = latch;
+ mIsOnResultExpected = isOnResultExpected;
+ }
+
+ @Override
+ public void onResult(Boolean result) {
+ if(mIsOnResultExpected) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onError(CallException error) {
+ OutcomeReceiver.super.onError(error);
+ if(!mIsOnResultExpected){
+ mCountDownLatch.countDown();
+ }
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void testCanHold() {
+ Call newCall = addSpyCall();
+ when(newCall.isTransactionalCall()).thenReturn(true);
+ when(newCall.can(Connection.CAPABILITY_SUPPORT_HOLD)).thenReturn(false);
+ assertFalse(mCallsManager.canHold(newCall));
+ when(newCall.can(Connection.CAPABILITY_SUPPORT_HOLD)).thenReturn(true);
+ assertTrue(mCallsManager.canHold(newCall));
+ }
+
+ @MediumTest
+ @Test
+ public void testOnFailedOutgoingCallRemovesCallImmediately() {
+ Call call = addSpyCall();
+ when(call.isDisconnectHandledViaFuture()).thenReturn(false);
+ CompletableFuture future = CompletableFuture.completedFuture(true);
+ when(mInCallController.getBindingFuture()).thenReturn(future);
+
+ mCallsManager.onFailedOutgoingCall(call, new DisconnectCause(DisconnectCause.OTHER));
+
+ future.join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+ assertFalse(mCallsManager.getCalls().contains(call));
+ }
+
+ @MediumTest
+ @Test
+ public void testHoldTransactional() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ 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);
+
+ // 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);
+
+ // case 3: cannot hold current active call early check
+ 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);
+
+ // case 4: activeCall != newCall && canHold(activeCall)
+ 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);
+ }
+
+ @SmallTest
+ @Test
+ public void testGetNumCallsWithState_MultiUser() throws Exception {
+ when(mContext.checkCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+ // Add call under secondary user
+ Call call = addSpyCall(SIM_1_HANDLE_SECONDARY, CallState.ACTIVE);
+ when(call.getPhoneAccountFromHandle()).thenReturn(SIM_1_ACCOUNT_SECONDARY);
+ // Verify that call is visible to primary user
+ assertEquals(mCallsManager.getNumCallsWithState(0, null,
+ UserHandle.CURRENT_OR_SELF, true,
+ null, CallState.ACTIVE), 1);
+ // Verify that call is not visible to primary user
+ // when a different phone account handle is specified.
+ assertEquals(mCallsManager.getNumCallsWithState(0, null,
+ UserHandle.CURRENT_OR_SELF, true,
+ SIM_1_HANDLE, CallState.ACTIVE), 0);
+ // Deny INTERACT_ACROSS_USERS permission and verify that call is not visible to primary user
+ assertEquals(mCallsManager.getNumCallsWithState(0, null,
+ UserHandle.CURRENT_OR_SELF, false,
+ null, CallState.ACTIVE), 0);
+ }
+
+ public void waitForCountDownLatch(CountDownLatch latch) throws InterruptedException {
+ boolean success = latch.await(5000, TimeUnit.MILLISECONDS);
+ if (!success) {
+ fail("assertOnResultWasReceived success failed");
+ }
+ }
+
+ /**
+ * When queryCurrentLocation is called, check whether the result is received through the
+ * ResultReceiver.
+ * @throws Exception if {@link CompletableFuture#get()} fails.
+ */
+ @Test
+ public void testQueryCurrentLocationCheckOnReceiveResult() throws Exception {
+ ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+ new ComponentName(mContext.getPackageName(),
+ mContext.getPackageName().getClass().getName()),
+ null, mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null);
+
+ CompletableFuture<String> resultFuture = new CompletableFuture<>();
+ try {
+ service.queryCurrentLocation(500L, "Test_provider",
+ new ResultReceiver(new Handler(Looper.getMainLooper())) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle result) {
+ super.onReceiveResult(resultCode, result);
+ resultFuture.complete("onReceiveResult");
+ }
+ });
+ } catch (Exception e) {
+ resultFuture.complete("Exception : " + e);
+ }
+
+ String result = resultFuture.get(1000L, TimeUnit.MILLISECONDS);
+ assertTrue(result.contains("onReceiveResult"));
+ }
+
+ @SmallTest
+ @Test
+ public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() {
+ Call existingCall = addSpyCall();
+ when(existingCall.getState()).thenReturn(CallState.ON_HOLD);
+
+ Call call = addSpyCall();
+ when(call.isDisconnectHandledViaFuture()).thenReturn(false);
+ when(call.isDisconnectingChildCall()).thenReturn(false);
+ CompletableFuture future = CompletableFuture.completedFuture(true);
+ when(mInCallController.getBindingFuture()).thenReturn(future);
+
+ mCallsManager.disconnectCall(call);
+ mCallsManager.onFailedOutgoingCall(call, new DisconnectCause(DisconnectCause.OTHER));
+
+ future.join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+ verify(existingCall).unhold();
+ }
+
+ @MediumTest
+ @Test
+ public void testOnFailedOutgoingCallUnholdsCallIfNoHoldButton() {
+ Call existingCall = addSpyCall();
+ when(existingCall.can(Connection.CAPABILITY_SUPPORT_HOLD)).thenReturn(false);
+ when(existingCall.getState()).thenReturn(CallState.ON_HOLD);
+
+ Call call = addSpyCall();
+ when(call.isDisconnectHandledViaFuture()).thenReturn(false);
+ CompletableFuture future = CompletableFuture.completedFuture(true);
+ when(mInCallController.getBindingFuture()).thenReturn(future);
+
+ mCallsManager.disconnectCall(call);
+ mCallsManager.onFailedOutgoingCall(call, new DisconnectCause(DisconnectCause.OTHER));
+
+ future.join();
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+ verify(existingCall).unhold();
+ }
+
+ @MediumTest
+ @Test
+ public void testOnCallFilteringCompleteRemovesUnwantedCallComposerAttachments() {
+ Call call = addSpyCall(CallState.NEW);
+ 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;
+ CallScreeningService.ParcelableCallResponse response =
+ mock(CallScreeningService.ParcelableCallResponse.class);
+ when(response.getCallComposerAttachmentsToShow()).thenReturn(attachmentDisabledMask);
+
+ CallFilteringResult result = new CallFilteringResult.Builder()
+ .setCallScreeningResponse(response, true)
+ .build();
+
+ mCallsManager.onCallFilteringComplete(call, result, false);
+
+ verify(extras).remove(TelecomManager.EXTRA_LOCATION);
+ verify(extras).remove(TelecomManager.EXTRA_CALL_SUBJECT);
+ verify(extras).remove(TelecomManager.EXTRA_PRIORITY);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnFailedIncomingCall() {
+ Call call = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+
+ mCallsManager.onFailedIncomingCall(call);
+
+ assertEquals(CallState.DISCONNECTED, call.getState());
+ verify(call).removeListener(mCallsManager);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnSuccessfulUnknownCall() {
+ Call call = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+
+ final int newState = CallState.ACTIVE;
+ mCallsManager.onSuccessfulUnknownCall(call, newState);
+
+ assertEquals(newState, call.getState());
+ assertTrue(mCallsManager.getCalls().contains(call));
+ }
+
+ @SmallTest
+ @Test
+ public void testOnFailedUnknownCall() {
+ Call call = createSpyCall(SIM_1_HANDLE, CallState.NEW);
+
+ mCallsManager.onFailedUnknownCall(call);
+
+ assertEquals(CallState.DISCONNECTED, call.getState());
+ verify(call).removeListener(mCallsManager);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnRingbackRequested() {
+ Call call = mock(Call.class);
+ final boolean ringback = true;
+
+ CallsManager.CallsManagerListener listener = mock(CallsManager.CallsManagerListener.class);
+ mCallsManager.addListener(listener);
+
+ mCallsManager.onRingbackRequested(call, ringback);
+
+ verify(listener).onRingbackRequested(call, ringback);
+ }
+
+ @MediumTest
+ @Test
+ public void testSetCallDialingAndDontIncreaseVolume() {
+ // Start with a non zero volume.
+ mComponentContextFixture.getAudioManager().setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+ 4, 0 /* flags */);
+
+ Call call = mock(Call.class);
+ mCallsManager.markCallAsDialing(call);
+
+ // We set the volume to non-zero above, so expect 1
+ verify(mComponentContextFixture.getAudioManager(), times(1)).setStreamVolume(
+ eq(AudioManager.STREAM_VOICE_CALL), anyInt(), anyInt());
+ }
+ @MediumTest
+ @Test
+ public void testSetCallDialingAndIncreaseVolume() {
+ // Start with a zero volume stream.
+ mComponentContextFixture.getAudioManager().setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+ 0, 0 /* flags */);
+
+ Call call = mock(Call.class);
+ mCallsManager.markCallAsDialing(call);
+
+ // We set the volume to zero above, so expect 2
+ verify(mComponentContextFixture.getAudioManager(), times(2)).setStreamVolume(
+ eq(AudioManager.STREAM_VOICE_CALL), anyInt(), anyInt());
+ }
+
+ @MediumTest
+ @Test
+ public void testSetCallActiveAndDontIncreaseVolume() {
+ // Start with a non-zero volume.
+ mComponentContextFixture.getAudioManager().setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+ 4, 0 /* flags */);
+
+ Call call = mock(Call.class);
+ mCallsManager.markCallAsActive(call);
+
+ // We set the volume to non-zero above, so expect 1 only.
+ verify(mComponentContextFixture.getAudioManager(), times(1)).setStreamVolume(
+ eq(AudioManager.STREAM_VOICE_CALL), anyInt(), anyInt());
+ }
+
+ @MediumTest
+ @Test
+ public void testHandoverToIsAccepted() {
+ Call sourceCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ when(call.getHandoverSourceCall()).thenReturn(sourceCall);
+ when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_TO_STARTED);
+
+ mCallsManager.createActionSetCallStateAndPerformAction(call, CallState.ACTIVE, "");
+
+ verify(call).setHandoverState(HandoverState.HANDOVER_ACCEPTED);
+ verify(call).onHandoverComplete();
+ verify(sourceCall).setHandoverState(HandoverState.HANDOVER_ACCEPTED);
+ verify(sourceCall).onHandoverComplete();
+ verify(sourceCall).disconnect();
+ }
+
+ @MediumTest
+ @Test
+ public void testSelfManagedHandoverToIsAccepted() {
+ Call sourceCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ Call call = addSpyCall(SELF_MANAGED_HANDLE, CallState.NEW);
+ when(call.getHandoverSourceCall()).thenReturn(sourceCall);
+ when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_TO_STARTED);
+ when(call.isSelfManaged()).thenReturn(true);
+ Call otherCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.ON_HOLD);
+
+ mCallsManager.createActionSetCallStateAndPerformAction(call, CallState.ACTIVE, "");
+
+ verify(call).setHandoverState(HandoverState.HANDOVER_ACCEPTED);
+ verify(call).onHandoverComplete();
+ verify(sourceCall).setHandoverState(HandoverState.HANDOVER_ACCEPTED);
+ verify(sourceCall).onHandoverComplete();
+ verify(sourceCall, times(2)).disconnect();
+ verify(otherCall).disconnect();
+ }
+
+ @SmallTest
+ @Test
+ public void testHandoverToIsRejected() {
+ Call sourceCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ when(call.getHandoverSourceCall()).thenReturn(sourceCall);
+ when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_TO_STARTED);
+ when(call.getConnectionService()).thenReturn(mock(ConnectionServiceWrapper.class));
+
+ 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);
+ }
+
+ @SmallTest
+ @Test
+ public void testHandoverFromIsStarted() {
+ Call destinationCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ 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
+ @Test
+ public void testHandoverFromIsAccepted() {
+ Call destinationCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ when(call.getHandoverDestinationCall()).thenReturn(destinationCall);
+ when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_ACCEPTED);
+
+ 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();
+ }
+
+ @SmallTest
+ @Test
+ public void testSelfManagedHandoverFromIsAccepted() {
+ Call destinationCall = addSpyCall(SELF_MANAGED_HANDLE, CallState.NEW);
+ when(destinationCall.isSelfManaged()).thenReturn(true);
+ Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
+ when(call.getHandoverDestinationCall()).thenReturn(destinationCall);
+ when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_ACCEPTED);
+ Call otherCall = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.ON_HOLD);
+
+ 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();
+ }
+
+ @MediumTest
+ @Test
+ public void testGetNumUnholdableCallsForOtherConnectionService() {
+ final int notDialingState = CallState.ACTIVE;
+ final PhoneAccountHandle accountHande = SIM_1_HANDLE;
+ assertFalse(mCallsManager.hasUnholdableCallsForOtherConnectionService(accountHande));
+
+ Call unholdableCall = addSpyCall(accountHande, notDialingState);
+ when(unholdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(false);
+ assertFalse(mCallsManager.hasUnholdableCallsForOtherConnectionService(accountHande));
+
+ Call holdableCall = addSpyCall(accountHande, notDialingState);
+ when(holdableCall.can(Connection.CAPABILITY_HOLD)).thenReturn(true);
+ assertFalse(mCallsManager.hasUnholdableCallsForOtherConnectionService(accountHande));
+
+ Call dialingCall = addSpyCall(accountHande, CallState.DIALING);
+ when(dialingCall.can(Connection.CAPABILITY_HOLD)).thenReturn(true);
+ assertFalse(mCallsManager.hasUnholdableCallsForOtherConnectionService(accountHande));
+
+ Call externalCall = addSpyCall(accountHande, notDialingState);
+ when(externalCall.isExternalCall()).thenReturn(true);
+ assertFalse(mCallsManager.hasUnholdableCallsForOtherConnectionService(accountHande));
+
+ Call unholdableOtherCall = addSpyCall(VOIP_1_HANDLE, notDialingState);
+ when(unholdableOtherCall.can(Connection.CAPABILITY_HOLD)).thenReturn(false);
+ assertTrue(mCallsManager.hasUnholdableCallsForOtherConnectionService(accountHande));
+ assertEquals(1, mCallsManager.getNumUnholdableCallsForOtherConnectionService(accountHande));
+ }
+
+ @SmallTest
+ @Test
+ public void testHasManagedCalls() {
+ assertFalse(mCallsManager.hasManagedCalls());
+
+ Call selfManagedCall = addSpyCall();
+ when(selfManagedCall.isSelfManaged()).thenReturn(true);
+ assertFalse(mCallsManager.hasManagedCalls());
+
+ Call externalCall = addSpyCall();
+ when(externalCall.isSelfManaged()).thenReturn(false);
+ when(externalCall.isExternalCall()).thenReturn(true);
+ assertFalse(mCallsManager.hasManagedCalls());
+
+ Call managedCall = addSpyCall();
+ when(managedCall.isSelfManaged()).thenReturn(false);
+ assertTrue(mCallsManager.hasManagedCalls());
+ }
+
+ @SmallTest
+ @Test
+ public void testHasSelfManagedCalls() {
+ Call managedCall = addSpyCall();
+ when(managedCall.isSelfManaged()).thenReturn(false);
+ assertFalse(mCallsManager.hasSelfManagedCalls());
+
+ Call selfManagedCall = addSpyCall();
+ when(selfManagedCall.isSelfManaged()).thenReturn(true);
+ assertTrue(mCallsManager.hasSelfManagedCalls());
+ }
+
+ /**
+ * Verifies when {@link CallsManager} receives a carrier config change it will trigger an
+ * update of the emergency call notification.
+ * Note: this test mocks out {@link BlockedNumbersAdapter} so does not actually test posting of
+ * the notification. Notification posting in the actual implementation is covered by
+ * {@link BlockedNumbersUtilTests}.
+ */
+ @SmallTest
+ @Test
+ public void testUpdateEmergencyCallNotificationOnCarrierConfigChange() {
+ when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
+ .thenReturn(true);
+ mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
+ new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_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(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)));
+ verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
+ eq(false));
+ }
+
+ /**
+ * Verifies when {@link CallsManager} receives a signal from the blocked number provider that
+ * the call blocking enabled state changes, it will trigger an update of the emergency call
+ * notification.
+ * Note: this test mocks out {@link BlockedNumbersAdapter} so does not actually test posting of
+ * the notification. Notification posting in the actual implementation is covered by
+ * {@link BlockedNumbersUtilTests}.
+ */
+ @SmallTest
+ @Test
+ public void testUpdateEmergencyCallNotificationOnNotificationVisibilityChange() {
+ when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
+ .thenReturn(true);
+ mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
+ new Intent(
+ BlockedNumberContract.SystemContract
+ .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)));
+ verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
+ eq(false));
+ }
+
+ /**
+ * Verify CallsManager#isInSelfManagedCall(packageName, userHandle) returns true when
+ * CallsManager is first made aware of the incoming call in processIncomingCallIntent.
+ */
+ @SmallTest
+ @Test
+ public void testAddNewIncomingCall_IsInSelfManagedCall() {
+ // GIVEN
+ assertEquals(0, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ assertFalse(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
+
+ // 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);
+
+ // THEN
+ mCallsManager.processIncomingCallIntent(SELF_MANAGED_W_CUSTOM_HANDLE, new Bundle(), false);
+
+ assertEquals(1, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ assertTrue(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
+ assertEquals(0, mCallsManager.getCalls().size());
+ }
+
+ /**
+ * Verify CallsManager#isInSelfManagedCall(packageName, userHandle) returns true when
+ * CallsManager is first made aware of the outgoing call in StartOutgoingCall.
+ */
+ @SmallTest
+ @Test
+ public void testStartOutgoing_IsInSelfManagedCall() {
+ // GIVEN
+ assertEquals(0, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ assertFalse(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
+
+ // WHEN
+ when(mPhoneAccountRegistrar.getPhoneAccount(any(), any()))
+ .thenReturn(SM_W_DIFFERENT_PACKAGE_AND_USER);
+ // Ensure contact info lookup succeeds
+ doAnswer(invocation -> {
+ Uri handle = invocation.getArgument(0);
+ CallerInfo info = new CallerInfo();
+ CompletableFuture<Pair<Uri, CallerInfo>> callerInfoFuture = new CompletableFuture<>();
+ callerInfoFuture.complete(new Pair<>(handle, info));
+ return callerInfoFuture;
+ }).when(mCallerInfoLookupHelper).startLookup(any(Uri.class));
+ // Ensure we have candidate phone account handle info.
+ when(mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(any(), any())).thenReturn(
+ SELF_MANAGED_W_CUSTOM_HANDLE);
+ when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
+ new ArrayList<>(List.of(SELF_MANAGED_W_CUSTOM_HANDLE)));
+
+ // THEN
+ mCallsManager.startOutgoingCall(TEST_ADDRESS, SELF_MANAGED_W_CUSTOM_HANDLE, new Bundle(),
+ TEST_USER_HANDLE, new Intent(), TEST_PACKAGE_NAME);
+
+ assertEquals(1, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ assertTrue(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
+ assertEquals(0, mCallsManager.getCalls().size());
+ }
+
+ /**
+ * Verify SelfManagedCallsBeingSetup is being cleaned up in CallsManager#addCall and
+ * CallsManager#removeCall. This ensures no memory leaks.
+ */
+ @SmallTest
+ @Test
+ public void testCallsBeingSetupCleanup() {
+ Call spyCall = addSpyCall();
+ assertEquals(0, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ // verify CallsManager#removeCall removes the call from SelfManagedCallsBeingSetup
+ mCallsManager.addCallBeingSetup(spyCall);
+ mCallsManager.removeCall(spyCall);
+ assertEquals(0, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ // verify CallsManager#addCall removes the call from SelfManagedCallsBeingSetup
+ mCallsManager.addCallBeingSetup(spyCall);
+ mCallsManager.addCall(spyCall);
+ assertEquals(0, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ }
+
+ /**
+ * Verify isInSelfManagedCall returns false if there is a self-managed call, but it is for a
+ * different package and user
+ */
+ @SmallTest
+ @Test
+ public void testIsInSelfManagedCall_PackageUserQueryIsWorkingAsIntended() {
+ // start an active call
+ Call randomCall = createSpyCall(SELF_MANAGED_HANDLE, CallState.ACTIVE);
+ mCallsManager.addCallBeingSetup(randomCall);
+ assertEquals(1, mCallsManager.getSelfManagedCallsBeingSetup().size());
+ // query isInSelfManagedCall for a package that is NOT in a call; expect false
+ assertFalse(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
+ // start another call
+ Call targetCall = addSpyCall(SELF_MANAGED_W_CUSTOM_HANDLE, CallState.DIALING);
+ when(targetCall.getTargetPhoneAccount()).thenReturn(SELF_MANAGED_W_CUSTOM_HANDLE);
+ when(targetCall.isSelfManaged()).thenReturn(true);
+ mCallsManager.addCallBeingSetup(targetCall);
+ // query isInSelfManagedCall for a package that is in a call
+ assertTrue(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
+ }
+
+
private Call addSpyCall() {
return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
}
@@ -1765,6 +3323,10 @@ public class CallsManagerTest extends TelecomTestCase {
mClockProxy,
mToastFactory);
ongoingCall.setState(initialState, "just cuz");
+ if (targetPhoneAccount == SELF_MANAGED_HANDLE
+ || targetPhoneAccount == SELF_MANAGED_2_HANDLE) {
+ ongoingCall.setIsSelfManaged(true);
+ }
return ongoingCall;
}
@@ -1780,9 +3342,15 @@ public class CallsManagerTest extends TelecomTestCase {
TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
when(mockTelephonyManager.getMaxNumberOfSimultaneouslyActiveSims()).thenReturn(1);
when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
- any(), anyInt(), anyInt())).thenReturn(
+ any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
when(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser()).thenReturn(
new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
}
+
+ private void setMaxActiveVoiceSubscriptions(int num) {
+ TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
+ when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability);
+ when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num);
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java b/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java
index 4ad46ae52..60567479b 100644
--- a/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java
@@ -236,6 +236,28 @@ public class CarModeTrackerTest extends TelecomTestCase {
}
/**
+ * Verifies that setting automotive projection overrides entering car mode with the highest
+ * priority of 0. Also ensures exiting car mode doesn't interfere with the automotive
+ * projection being set.
+ */
+ @Test
+ public void testInterleaveCarModeAndProjectionMode() {
+ mCarModeTracker.handleEnterCarMode(0, CAR_MODE_APP1_PACKAGE_NAME);
+ assertEquals(CAR_MODE_APP1_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ assertTrue(mCarModeTracker.isInCarMode());
+
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP2_PACKAGE_NAME);
+ assertEquals(CAR_MODE_APP2_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ assertTrue(mCarModeTracker.isInCarMode());
+
+ mCarModeTracker.handleExitCarMode(0, CAR_MODE_APP1_PACKAGE_NAME);
+ assertEquals(CAR_MODE_APP2_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ assertTrue(mCarModeTracker.isInCarMode());
+
+ mCarModeTracker.handleReleaseAutomotiveProjection();
+ }
+
+ /**
* Verifies that if we set automotive projection more than once with the same package, nothing
* changes.
*/
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 477ca9fcd..cc22de291 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -52,11 +52,16 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.hardware.SensorPrivacyManager;
import android.location.CountryDetector;
+import android.location.LocationManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.os.BugreportManager;
import android.os.Bundle;
+import android.os.DropBoxManager;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IInterface;
+import android.os.Looper;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
@@ -74,6 +79,7 @@ import android.telephony.TelephonyManager;
import android.telephony.TelephonyRegistryManager;
import android.test.mock.MockContext;
import android.util.DisplayMetrics;
+import android.view.accessibility.AccessibilityManager;
import java.io.File;
import java.io.IOException;
@@ -85,6 +91,8 @@ import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
+import static android.content.Context.DEVICE_ID_DEFAULT;
+
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.ArgumentMatchers.nullable;
@@ -106,6 +114,7 @@ import static org.mockito.Mockito.when;
* property points to an application context implementing all the nontrivial functionality.
*/
public class ComponentContextFixture implements TestFixture<Context> {
+ private HandlerThread mHandlerThread;
public class FakeApplicationContext extends MockContext {
@Override
@@ -126,6 +135,9 @@ public class ComponentContextFixture implements TestFixture<Context> {
}
@Override
+ public Context createAttributionContext(String attributionTag) { return this; }
+
+ @Override
public String getPackageName() {
return "com.android.server.telecom.tests";
}
@@ -199,6 +211,8 @@ public class ComponentContextFixture implements TestFixture<Context> {
return mAudioManager;
case Context.TELEPHONY_SERVICE:
return mTelephonyManager;
+ case Context.LOCATION_SERVICE:
+ return mLocationManager;
case Context.APP_OPS_SERVICE:
return mAppOpsManager;
case Context.NOTIFICATION_SERVICE:
@@ -229,6 +243,8 @@ public class ComponentContextFixture implements TestFixture<Context> {
return mPermissionCheckerManager;
case Context.SENSOR_PRIVACY_SERVICE:
return mSensorPrivacyManager;
+ case Context.ACCESSIBILITY_SERVICE:
+ return mAccessibilityManager;
default:
return null;
}
@@ -262,8 +278,14 @@ public class ComponentContextFixture implements TestFixture<Context> {
return Context.SENSOR_PRIVACY_SERVICE;
} else if (svcClass == NotificationManager.class) {
return Context.NOTIFICATION_SERVICE;
+ } else if (svcClass == AccessibilityManager.class) {
+ return Context.ACCESSIBILITY_SERVICE;
+ } else if (svcClass == DropBoxManager.class) {
+ return Context.DROPBOX_SERVICE;
+ } else if (svcClass == BugreportManager.class) {
+ return Context.BUGREPORT_SERVICE;
}
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException(svcClass.getName());
}
@Override
@@ -292,6 +314,15 @@ public class ComponentContextFixture implements TestFixture<Context> {
}
@Override
+ public Looper getMainLooper() {
+ if (mHandlerThread == null) {
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+ }
+ return mHandlerThread.getLooper();
+ }
+
+ @Override
public ContentResolver getContentResolver() {
return new ContentResolver(mApplicationContextSpy) {
@Override
@@ -331,30 +362,34 @@ public class ComponentContextFixture implements TestFixture<Context> {
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- // TODO -- this is called by WiredHeadsetManager!!!
+ mBroadcastReceivers.add(receiver);
return null;
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+ mBroadcastReceivers.add(receiver);
return null;
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
String broadcastPermission, Handler scheduler) {
+ mBroadcastReceivers.add(receiver);
return null;
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
String broadcastPermission, Handler scheduler, int flags) {
+ mBroadcastReceivers.add(receiver);
return null;
}
@Override
public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle handle,
IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ mBroadcastReceivers.add(receiver);
return null;
}
@@ -534,6 +569,11 @@ public class ComponentContextFixture implements TestFixture<Context> {
public Resources getResources() {
return mResources;
}
+
+ @Override
+ public int getDeviceId() {
+ return DEVICE_ID_DEFAULT;
+ }
};
// The application context is the most important object this class provides to the system
@@ -551,8 +591,10 @@ public class ComponentContextFixture implements TestFixture<Context> {
private final Executor mMainExecutor = mock(Executor.class);
private final AudioManager mAudioManager = spy(new FakeAudioManager(mContext));
private final TelephonyManager mTelephonyManager = mock(TelephonyManager.class);
+ private final LocationManager mLocationManager = mock(LocationManager.class);
private final AppOpsManager mAppOpsManager = mock(AppOpsManager.class);
private final NotificationManager mNotificationManager = mock(NotificationManager.class);
+ private final AccessibilityManager mAccessibilityManager = mock(AccessibilityManager.class);
private final UserManager mUserManager = mock(UserManager.class);
private final StatusBarManager mStatusBarManager = mock(StatusBarManager.class);
private SubscriptionManager mSubscriptionManager = mock(SubscriptionManager.class);
@@ -571,6 +613,7 @@ public class ComponentContextFixture implements TestFixture<Context> {
mock(PermissionCheckerManager.class);
private final PermissionInfo mPermissionInfo = mock(PermissionInfo.class);
private final SensorPrivacyManager mSensorPrivacyManager = mock(SensorPrivacyManager.class);
+ private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
private TelecomManager mTelecomManager = mock(TelecomManager.class);
@@ -763,6 +806,10 @@ public class ComponentContextFixture implements TestFixture<Context> {
return mTelephonyManager;
}
+ public AudioManager getAudioManager() {
+ return mAudioManager;
+ }
+
public CarrierConfigManager getCarrierConfigManager() {
return mCarrierConfigManager;
}
@@ -771,6 +818,10 @@ public class ComponentContextFixture implements TestFixture<Context> {
return mNotificationManager;
}
+ public List<BroadcastReceiver> getBroadcastReceivers() {
+ return mBroadcastReceivers;
+ }
+
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/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 6e6646f7a..0927b80d7 100755
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -34,6 +34,7 @@ import android.os.IInterface;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
import android.telecom.CallScreeningService;
import android.telecom.Conference;
import android.telecom.Connection;
@@ -68,6 +69,9 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
static int INVALID_VIDEO_STATE = -1;
public CountDownLatch mExtrasLock = new CountDownLatch(1);
static int NOT_SPECIFIED = 0;
+ public static final String STATUS_HINTS_EXTRA = "updateStatusHints";
+ public static final PhoneAccountHandle TEST_PHONE_ACCOUNT_HANDLE =
+ new PhoneAccountHandle(new ComponentName("pkg", "cls"), "test");
/**
* Implementation of ConnectionService that performs no-ops for tasks normally meant for
@@ -102,6 +106,11 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
if (mProperties != NOT_SPECIFIED) {
fakeConnection.setConnectionProperties(mProperties);
}
+ // Testing for StatusHints image icon cross user access
+ if (request.getExtras() != null) {
+ fakeConnection.setStatusHints(
+ request.getExtras().getParcelable(STATUS_HINTS_EXTRA));
+ }
return fakeConnection;
}
@@ -118,6 +127,11 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
if (mProperties != NOT_SPECIFIED) {
fakeConnection.setConnectionProperties(mProperties);
}
+ // Testing for StatusHints image icon cross user access
+ if (request.getExtras() != null) {
+ fakeConnection.setStatusHints(
+ request.getExtras().getParcelable(STATUS_HINTS_EXTRA));
+ }
return fakeConnection;
}
@@ -134,6 +148,12 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
Conference fakeConference = new FakeConference();
fakeConference.addConnection(cxn1);
fakeConference.addConnection(cxn2);
+ if (cxn1.getStatusHints() != null || cxn2.getStatusHints() != null) {
+ // For testing purposes, pick one of the status hints that isn't null.
+ StatusHints statusHints = cxn1.getStatusHints() != null
+ ? cxn1.getStatusHints() : cxn2.getStatusHints();
+ fakeConference.setStatusHints(statusHints);
+ }
mLatestConference = fakeConference;
addConference(fakeConference);
} else {
@@ -178,7 +198,7 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
public class FakeConference extends Conference {
public FakeConference() {
- super(null);
+ super(TEST_PHONE_ACCOUNT_HANDLE);
setConnectionCapabilities(
Connection.CAPABILITY_SUPPORT_HOLD
| Connection.CAPABILITY_HOLD
@@ -343,6 +363,18 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
throws RemoteException { }
@Override
+ public void onCallEndpointChanged(String callId, CallEndpoint callEndpoint,
+ Session.Info sessionInfo) { }
+
+ @Override
+ public void onAvailableCallEndpointsChanged(String callId,
+ List<CallEndpoint> availableCallEndpoints, Session.Info sessionInfo) { }
+
+ @Override
+ public void onMuteStateChanged(String callId, boolean isMuted,
+ Session.Info sessionInfo) { }
+
+ @Override
public void onUsingAlternativeUi(String activeCallId, boolean usingAlternativeUi,
Session.Info info) throws RemoteException { }
@@ -494,6 +526,7 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
public String mLatestConnectionId;
public Connection mLatestConnection;
+ public ParcelableConnection mLatestParcelableConnection;
public Conference mLatestConference;
public final Set<IConnectionServiceAdapter> mConnectionServiceAdapters = new HashSet<>();
public final Map<String, ConnectionInfo> mConnectionById = new HashMap<>();
@@ -738,7 +771,7 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
}
private ParcelableConnection parcelable(ConnectionInfo c) {
- return new ParcelableConnection(
+ mLatestParcelableConnection = new ParcelableConnection(
c.request.getAccountHandle(),
c.state,
c.capabilities,
@@ -759,5 +792,6 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
c.conferenceableConnectionIds,
c.extras,
c.callerNumberVerificationStatus);
+ return mLatestParcelableConnection;
}
}
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index cb376af98..8a85a8786 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,8 +16,10 @@
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;
@@ -55,6 +57,8 @@ import java.util.Random;
import java.util.UUID;
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;
@@ -75,6 +79,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
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 UserHandle USER_HANDLE_10 = new UserHandle(10);
@Mock
ConnectionServiceRepository mMockConnectionServiceRepository;
@@ -137,7 +142,10 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
SubscriptionManager.INVALID_SIM_SLOT_INDEX);
}
});
- when(mMockAccountRegistrar.getAllPhoneAccountsOfCurrentUser()).thenReturn(phoneAccounts);
+ when(mMockAccountRegistrar.getAllPhoneAccounts(any(UserHandle.class), anyBoolean()))
+ .thenReturn(phoneAccounts);
+ when(mMockCall.getAssociatedUser()).
+ thenReturn(Binder.getCallingUserHandle());
}
@Override
@@ -200,7 +208,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
// Make sure the target phone account has the correct permissions
PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
- PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
mFakeTargetPhoneAccount);
@@ -229,7 +237,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
- PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
mFakeTargetPhoneAccount);
when(mMockCall.getConnectionService()).thenReturn(service);
@@ -269,7 +277,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
- PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
mFakeTargetPhoneAccount);
when(mMockCall.getConnectionService()).thenReturn(service);
@@ -315,7 +323,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
// Do not use this account, even though it is a SIM subscription and can place emergency
// calls
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0);
+ PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0, null);
mapToSubSlot(emergencyPhoneAccount, 2 /*subId*/, 1 /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount);
@@ -332,6 +340,36 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
}
/**
+ * 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).
+ */
+ @SmallTest
+ @Test
+ public void testEmergencyCallAcrossUsers() throws Exception {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ // Add an emergency account associated with a different user and expect this to be called.
+ PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer",
+ 0, USER_HANDLE_10);
+ mapToSubSlot(emergencyPhoneAccount, 1 /*subId*/, 0 /*slotId*/);
+ phoneAccounts.add(emergencyPhoneAccount);
+ PhoneAccountHandle emergencyPhoneAccountHandle = emergencyPhoneAccount.getAccountHandle();
+
+ mTestCreateConnectionProcessor.process();
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(eq(emergencyPhoneAccountHandle));
+ verify(mMockCall).setTargetPhoneAccount(eq(emergencyPhoneAccountHandle));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ // Notify successful connection to call
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ /**
* 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.
*/
@@ -351,7 +389,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
"cm_acct", 0);
phoneAccounts.add(callManagerPA);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0);
+ PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0, null);
mapToSubSlot(emergencyPhoneAccount, 2 /*subId*/, 1 /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount);
PhoneAccountHandle emergencyPhoneAccountHandle = emergencyPhoneAccount.getAccountHandle();
@@ -387,10 +425,10 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
"cm_acct", 0);
phoneAccounts.add(callManagerPA);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0);
+ PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0, null);
phoneAccounts.add(emergencyPhoneAccount1);
mapToSubSlot(emergencyPhoneAccount1, 1 /*subId*/, 1 /*slotId*/);
- PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0);
+ PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0, null);
phoneAccounts.add(emergencyPhoneAccount2);
mapToSubSlot(emergencyPhoneAccount2, 2 /*subId*/, 0 /*slotId*/);
PhoneAccountHandle emergencyPhoneAccountHandle2 = emergencyPhoneAccount2.getAccountHandle();
@@ -418,12 +456,12 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
when(mMockCall.isEmergencyCall()).thenReturn(true);
when(mMockCall.isTestEmergencyCall()).thenReturn(false);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0);
+ PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0, null);
mapToSubSlot(emergencyPhoneAccount1, 1 /*subId*/, 0 /*slotId*/);
setTargetPhoneAccount(mMockCall, emergencyPhoneAccount1.getAccountHandle());
phoneAccounts.add(emergencyPhoneAccount1);
PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2",
- PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED);
+ PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED, null);
mapToSubSlot(emergencyPhoneAccount2, 2 /*subId*/, 1 /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount2);
PhoneAccountHandle emergencyPhoneAccountHandle2 = emergencyPhoneAccount2.getAccountHandle();
@@ -455,10 +493,10 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
"cm_acct", 0);
phoneAccounts.add(callManagerPA);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0);
+ PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0, null);
mapToSubSlot(emergencyPhoneAccount1, 1 /*subId*/, 0 /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount1);
- PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0);
+ PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0, null);
// Make this the user preferred account
mapToSubSlot(emergencyPhoneAccount2, 2 /*subId*/, 1 /*slotId*/);
setTargetPhoneAccount(mMockCall, emergencyPhoneAccount2.getAccountHandle());
@@ -479,6 +517,43 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
}
/**
+ * Ensure that the call goes out on the PhoneAccount for the incoming call and not the
+ * Telephony preferred emergency account.
+ */
+ @SmallTest
+ @Test
+ public void testMTEmergencyCallMultiSimUserPreferred() throws Exception {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+ when(mMockCall.isIncoming()).thenReturn(true);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0, null);
+ mapToSubSlot(emergencyPhoneAccount1, 1 /*subId*/, 0 /*slotId*/);
+ setTargetPhoneAccount(mMockCall, emergencyPhoneAccount1.getAccountHandle());
+ phoneAccounts.add(emergencyPhoneAccount1);
+ // Make this the user preferred phone account
+ setTargetPhoneAccount(mMockCall, emergencyPhoneAccount1.getAccountHandle());
+ PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2",
+ PhoneAccount.CAPABILITY_EMERGENCY_PREFERRED, null);
+ mapToSubSlot(emergencyPhoneAccount2, 2 /*subId*/, 1 /*slotId*/);
+ phoneAccounts.add(emergencyPhoneAccount2);
+ PhoneAccountHandle emergencyPhoneAccountHandle2 = emergencyPhoneAccount2.getAccountHandle();
+
+ mTestCreateConnectionProcessor.process();
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(
+ eq(emergencyPhoneAccount1.getAccountHandle()));
+ // The account we're using to place the call should be the user preferred account
+ verify(mMockCall).setTargetPhoneAccount(eq(emergencyPhoneAccount1.getAccountHandle()));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ // Notify successful connection to call
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ /**
* If the user preferred PhoneAccount is associated with an invalid slot, place on the other,
* valid slot.
*/
@@ -492,13 +567,13 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
"cm_acct", 0);
phoneAccounts.add(callManagerPA);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0);
+ PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0, null);
// make this the user preferred account
setTargetPhoneAccount(mMockCall, emergencyPhoneAccount1.getAccountHandle());
mapToSubSlot(emergencyPhoneAccount1, 1 /*subId*/,
SubscriptionManager.INVALID_SIM_SLOT_INDEX /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount1);
- PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0);
+ PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0, null);
mapToSubSlot(emergencyPhoneAccount2, 2 /*subId*/, 1 /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount2);
PhoneAccountHandle emergencyPhoneAccountHandle2 = emergencyPhoneAccount2.getAccountHandle();
@@ -529,11 +604,11 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
"cm_acct", 0);
phoneAccounts.add(callManagerPA);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0);
+ PhoneAccount emergencyPhoneAccount1 = makeEmergencyPhoneAccount("tel_emer1", 0, null);
mapToSubSlot(emergencyPhoneAccount1, 1 /*subId*/,
SubscriptionManager.INVALID_SIM_SLOT_INDEX /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount1);
- PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0);
+ PhoneAccount emergencyPhoneAccount2 = makeEmergencyPhoneAccount("tel_emer2", 0, null);
mapToSubSlot(emergencyPhoneAccount2, 2 /*subId*/, 1 /*slotId*/);
phoneAccounts.add(emergencyPhoneAccount2);
PhoneAccountHandle emergencyPhoneAccountHandle2 = emergencyPhoneAccount2.getAccountHandle();
@@ -593,7 +668,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
PhoneAccount emerCallManagerPA = getNewEmergencyConnectionManagerPhoneAccount("cm_acct",
PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
ConnectionServiceWrapper service = makeConnectionServiceWrapper();
- PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0);
+ PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer", 0, null);
phoneAccounts.add(emergencyPhoneAccount);
mapToSubSlot(regularAccount, 2 /*subId*/, 1 /*slotId*/);
mTestCreateConnectionProcessor.process();
@@ -682,7 +757,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
private PhoneAccount makeEmergencyTestPhoneAccount(String id, int capabilities) {
final PhoneAccount emergencyPhoneAccount = makeQuickAccount(id, capabilities |
- PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+ PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS, null);
PhoneAccountHandle emergencyPhoneAccountHandle = emergencyPhoneAccount.getAccountHandle();
givePhoneAccountBindPermission(emergencyPhoneAccountHandle);
when(mMockAccountRegistrar.getPhoneAccountUnchecked(emergencyPhoneAccountHandle))
@@ -690,10 +765,11 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
return emergencyPhoneAccount;
}
- private PhoneAccount makeEmergencyPhoneAccount(String id, int capabilities) {
+ private PhoneAccount makeEmergencyPhoneAccount(String id, int capabilities,
+ UserHandle userHandle) {
final PhoneAccount emergencyPhoneAccount = makeQuickAccount(id, capabilities |
PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS |
- PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, userHandle);
PhoneAccountHandle emergencyPhoneAccountHandle = emergencyPhoneAccount.getAccountHandle();
givePhoneAccountBindPermission(emergencyPhoneAccountHandle);
when(mMockAccountRegistrar.getPhoneAccountUnchecked(emergencyPhoneAccountHandle))
@@ -702,7 +778,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
}
private PhoneAccount makePhoneAccount(String id, int capabilities) {
- final PhoneAccount phoneAccount = makeQuickAccount(id, capabilities);
+ final PhoneAccount phoneAccount = makeQuickAccount(id, capabilities, null);
PhoneAccountHandle phoneAccountHandle = phoneAccount.getAccountHandle();
givePhoneAccountBindPermission(phoneAccountHandle);
when(mMockAccountRegistrar.getPhoneAccountUnchecked(
@@ -720,7 +796,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
}
private PhoneAccountHandle getNewConnectionMangerHandleForCall(Call call, String id) {
- PhoneAccountHandle callManagerPAHandle = makeQuickAccountHandle(id);
+ PhoneAccountHandle callManagerPAHandle = makeQuickAccountHandle(id, null);
when(mMockAccountRegistrar.getSimCallManagerFromCall(eq(call))).thenReturn(
callManagerPAHandle);
givePhoneAccountBindPermission(callManagerPAHandle);
@@ -728,7 +804,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
}
private PhoneAccountHandle getNewTargetPhoneAccountHandle(String id) {
- PhoneAccountHandle pAHandle = makeQuickAccountHandle(id);
+ PhoneAccountHandle pAHandle = makeQuickAccountHandle(id, null);
givePhoneAccountBindPermission(pAHandle);
return pAHandle;
}
@@ -739,7 +815,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
private PhoneAccount createNewConnectionManagerPhoneAccountForCall(Call call, String id,
int capability) {
- PhoneAccount callManagerPA = makeQuickAccount(id, capability);
+ PhoneAccount callManagerPA = makeQuickAccount(id, capability, null);
when(mMockAccountRegistrar.getSimCallManagerFromCall(eq(call))).thenReturn(
callManagerPA.getAccountHandle());
givePhoneAccountBindPermission(callManagerPA.getAccountHandle());
@@ -749,7 +825,7 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
}
private PhoneAccount getNewEmergencyConnectionManagerPhoneAccount(String id, int capability) {
- PhoneAccount callManagerPA = makeQuickAccount(id, capability);
+ PhoneAccount callManagerPA = makeQuickAccount(id, capability, null);
when(mMockAccountRegistrar.getSimCallManagerOfCurrentUser()).thenReturn(
callManagerPA.getAccountHandle());
givePhoneAccountBindPermission(callManagerPA.getAccountHandle());
@@ -766,21 +842,24 @@ public class CreateConnectionProcessorTest extends TelecomTestCase {
ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
when(mMockConnectionServiceRepository.getService(
eq(makeQuickConnectionServiceComponentName()),
- eq(Binder.getCallingUserHandle()))).thenReturn(wrapper);
+ any(UserHandle.class))).thenReturn(wrapper);
return wrapper;
}
- private static PhoneAccountHandle makeQuickAccountHandle(String id) {
- return new PhoneAccountHandle(makeQuickConnectionServiceComponentName(), id,
- Binder.getCallingUserHandle());
+ private static PhoneAccountHandle makeQuickAccountHandle(String id, UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = Binder.getCallingUserHandle();
+ }
+ return new PhoneAccountHandle(makeQuickConnectionServiceComponentName(), id, userHandle);
}
- private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
- return new PhoneAccount.Builder(makeQuickAccountHandle(id), "label" + idx);
+ private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx,
+ UserHandle userHandle) {
+ return new PhoneAccount.Builder(makeQuickAccountHandle(id, userHandle), "label" + idx);
}
- private PhoneAccount makeQuickAccount(String id, int idx) {
- return makeQuickAccountBuilder(id, idx)
+ private PhoneAccount makeQuickAccount(String id, int idx, UserHandle userHandle) {
+ return makeQuickAccountBuilder(id, idx, userHandle)
.setAddress(Uri.parse("http://foo.com/" + idx))
.setSubscriptionAddress(Uri.parse("tel:555-000" + idx))
.setCapabilities(idx)
diff --git a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
index 2cdc23af7..05c5071b5 100644
--- a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
@@ -151,6 +151,7 @@ public class DisconnectedCallNotifierTest extends TelecomTestCase {
when(call.getDisconnectCause()).thenReturn(cause);
when(call.getTargetPhoneAccount()).thenReturn(PHONE_ACCOUNT_HANDLE);
when(call.getHandle()).thenReturn(TEL_CALL_HANDLE);
+ when(call.getAssociatedUser()).thenReturn(PHONE_ACCOUNT_HANDLE.getUserHandle());
return call;
}
}
diff --git a/tests/src/com/android/server/telecom/tests/DndCallFilteringTests.java b/tests/src/com/android/server/telecom/tests/DndCallFilteringTests.java
new file mode 100644
index 000000000..4885d6118
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/DndCallFilteringTests.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.net.Uri;
+
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.Ringer;
+import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.callfiltering.DndCallFilter;
+
+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.Mock;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import junit.framework.Assert;
+
+@RunWith(JUnit4.class)
+public class DndCallFilteringTests extends TelecomTestCase {
+
+ // mocks
+ @Mock private Call mCall;
+ @Mock private Ringer mRinger;
+ // constants
+ private final long FILTER_TIMEOUT = 2000;
+
+ private final CallFilteringResult BASE_RESULT = new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true)
+ .build();
+
+
+ private final CallFilteringResult CALL_SUPPRESSED_RESULT = new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true)
+ .setDndSuppressed(true)
+ .build();
+
+ private final CallFilteringResult CALL_NOT_SUPPRESSED_RESULT = new CallFilteringResult.Builder()
+ .setShouldAllowCall(true)
+ .setShouldAddToCallLog(true)
+ .setShouldShowNotification(true)
+ .setDndSuppressed(false)
+ .build();
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ // Dynamic variables
+ Uri testHandle = Uri.parse("tel:1235551234");
+ when(mCall.getHandle()).thenReturn(testHandle);
+ when(mCall.wasDndCheckComputedForCall()).thenReturn(false);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Test DndCallFilter suppresses a call and builds a CALL_SUPPRESSED_RESULT when given
+ * a false shouldRingForContact answer.
+ *
+ * @throws Exception; should not throw
+ */
+ @Test
+ public void testShouldSuppressCall() throws Exception {
+ // GIVEN
+ DndCallFilter filter = new DndCallFilter(mCall, mRinger);
+
+ // WHEN
+ assertNotNull(filter);
+ when(mRinger.shouldRingForContact(mCall)).thenReturn(false);
+
+ // THEN
+ CompletionStage<CallFilteringResult> resultFuture = filter.startFilterLookup(BASE_RESULT);
+
+ Assert.assertEquals(CALL_SUPPRESSED_RESULT, resultFuture.toCompletableFuture()
+ .get(FILTER_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ /**
+ * Test DndCallFilter allows a call to ring and builds a CALL_NOT_SUPPRESSED_RESULT when
+ * given a true shouldRingForContact answer.
+ *
+ * @throws Exception; should not throw
+ */
+ @Test
+ public void testCallShouldRingAndNotBeSuppressed() throws Exception {
+ // GIVEN
+ DndCallFilter filter = new DndCallFilter(mCall, mRinger);
+
+ // WHEN
+ assertNotNull(filter);
+ when(mRinger.shouldRingForContact(mCall)).thenReturn(true);
+
+ // THEN
+ CompletionStage<CallFilteringResult> resultFuture = filter.startFilterLookup(BASE_RESULT);
+
+ // ASSERT
+ Assert.assertEquals(CALL_NOT_SUPPRESSED_RESULT, resultFuture.toCompletableFuture()
+ .get(FILTER_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+} \ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
new file mode 100644
index 000000000..3cb819672
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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 android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+
+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.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+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.net.Uri;
+import android.os.BugreportManager;
+import android.os.DropBoxManager;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+
+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.EmergencyCallDiagnosticLogger;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.ui.ToastFactory;
+
+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 EmergencyCallDiagnosticLoggerTest extends TelecomTestCase {
+
+ private static final ComponentName COMPONENT_NAME_1 = ComponentName
+ .unflattenFromString("com.foo/.Blah");
+ private static final PhoneAccountHandle SIM_1_HANDLE = new PhoneAccountHandle(
+ COMPONENT_NAME_1, "Sim1");
+ private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.
+ Builder(SIM_1_HANDLE, "Sim1")
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setIsEnabled(true)
+ .build();
+ private static final String DROP_BOX_TAG = "ecall_diagnostic_data";
+
+ private static final long EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS = 100L;
+
+ private static final long EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS = 120L;
+
+ private static final int DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES = 1;
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {
+ };
+ EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
+ @Mock
+ private Timeouts.Adapter mTimeouts;
+ @Mock
+ private CallsManager mMockCallsManager;
+ @Mock
+ private CallerInfoLookupHelper mMockCallerInfoLookupHelper;
+ @Mock
+ private PhoneAccountRegistrar mMockPhoneAccountRegistrar;
+ @Mock
+ private ClockProxy mMockClockProxy;
+ @Mock
+ private ToastFactory mMockToastProxy;
+ @Mock
+ private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
+
+ @Mock
+ private TelephonyManager mTm;
+ @Mock
+ private BugreportManager mBrm;
+ @Mock
+ private DropBoxManager mDbm;
+
+ @Mock
+ private ClockProxy mClockProxy;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ doReturn(mMockCallerInfoLookupHelper).when(mMockCallsManager).getCallerInfoLookupHelper();
+ doReturn(mMockPhoneAccountRegistrar).when(mMockCallsManager).getPhoneAccountRegistrar();
+ doReturn(SIM_1_ACCOUNT).when(mMockPhoneAccountRegistrar).getPhoneAccountUnchecked(
+ eq(SIM_1_HANDLE));
+ when(mTimeouts.getEmergencyCallActiveTimeThresholdMillis()).
+ thenReturn(EMERGENCY_CALL_ACTIVE_TIME_THRESHOLD_MILLIS);
+ when(mTimeouts.getEmergencyCallTimeBeforeUserDisconnectThresholdMillis()).
+ thenReturn(EMERGENCY_CALL_TIME_BEFORE_USER_DISCONNECT_THRESHOLD_MILLIS);
+ when(mTimeouts.getDaysBackToSearchEmergencyDiagnosticEntries()).
+ thenReturn(DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES);
+ when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
+
+ mEmergencyCallDiagnosticLogger = new EmergencyCallDiagnosticLogger(mTm, mBrm,
+ mTimeouts, mDbm, Runnable::run, mClockProxy);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ //reset(mTm);
+ }
+
+ /**
+ * Helper function that creates the call being tested.
+ * Also invokes onStartCreateConnection
+ */
+ private Call createCall(boolean isEmergencyCall, int direction) {
+ Call call = getCall();
+ call.setCallDirection(direction);
+ call.setIsEmergencyCall(isEmergencyCall);
+ mEmergencyCallDiagnosticLogger.onStartCreateConnection(call);
+ return call;
+ }
+
+ /**
+ * @return an instance of {@link Call} for testing purposes.
+ */
+ private Call getCall() {
+ return new Call(
+ "1", /* callId */
+ mContext,
+ mMockCallsManager,
+ mLock,
+ null /* ConnectionServiceRepository */,
+ mMockPhoneNumberUtilsAdapter,
+ Uri.parse("tel:6505551212"),
+ null /* GatewayInfo */,
+ null /* connectionManagerPhoneAccountHandle */,
+ SIM_1_HANDLE,
+ Call.CALL_DIRECTION_OUTGOING,
+ false /* shouldAttachToExistingConnection*/,
+ false /* isConference */,
+ mMockClockProxy,
+ mMockToastProxy);
+ }
+
+ /**
+ * Test that only outgoing emergency calls are tracked
+ */
+ @Test
+ public void testNonEmergencyCallNotTracked() {
+ //should not be tracked
+ createCall(false, Call.CALL_DIRECTION_OUTGOING);
+ assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+
+ //should not be tracked (not in scope)
+ createCall(false, Call.CALL_DIRECTION_INCOMING);
+ assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+ }
+
+ /**
+ * Test that incoming emergency calls are not tracked (not in scope right now)
+ */
+ @Test
+ public void testIncomingEmergencyCallsNotTracked() {
+ //should not be tracked
+ createCall(true, Call.CALL_DIRECTION_INCOMING);
+ assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+ }
+
+
+ /**
+ * Test getDataCollectionTypes(reason)
+ */
+ @Test
+ public void testCollectionTypeForReasonDoesNotReturnUnreasonableValues() {
+ int reason = EmergencyCallDiagnosticLogger.REPORT_REASON_RANGE_START + 1;
+ while (reason < EmergencyCallDiagnosticLogger.REPORT_REASON_RANGE_END) {
+ List<Integer> ctypes = EmergencyCallDiagnosticLogger.getDataCollectionTypes(reason);
+ assertNotNull(ctypes);
+ Set<Integer> ctypesSet = new HashSet<>(ctypes);
+
+ //assert that list is not empty
+ assertNotEquals(0, ctypes.size());
+
+ //assert no repeated values
+ assertEquals(ctypes.size(), ctypesSet.size());
+
+ //if bugreport type is present, that should be the only collection type
+ if (ctypesSet.contains(EmergencyCallDiagnosticLogger.COLLECTION_TYPE_BUGREPORT)) {
+ assertEquals(1, ctypes.size());
+ }
+ reason++;
+ }
+ }
+
+
+ /**
+ * Test emergency call reported stuck
+ */
+ @Test
+ public void testStuckEmergencyCall() {
+ Call call = createCall(true, Call.CALL_DIRECTION_OUTGOING);
+ mEmergencyCallDiagnosticLogger.onCallAdded(call);
+ mEmergencyCallDiagnosticLogger.reportStuckCall(call);
+
+ //for stuck calls, we should always be persisting some data
+ ArgumentCaptor<EmergencyCallDiagnosticParams> captor =
+ ArgumentCaptor.forClass(EmergencyCallDiagnosticParams.class);
+ verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
+ captor.capture());
+ EmergencyCallDiagnosticParams dp = captor.getValue();
+
+ assertNotNull(dp);
+ assertTrue(
+ dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
+ || dp.isTelephonyDumpSysCollectionEnabled());
+
+ //tracking should end
+ assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+ }
+
+ @Test
+ public void testEmergencyCallNeverWentActiveWithNonLocalDisconnectCause() {
+ Call call = createCall(true, Call.CALL_DIRECTION_OUTGOING);
+ mEmergencyCallDiagnosticLogger.onCallAdded(call);
+
+ //call is tracked
+ assertEquals(1, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+
+ call.setDisconnectCause(new DisconnectCause(DisconnectCause.REJECTED));
+ 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);
+ verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
+ captor.capture());
+ TelephonyManager.EmergencyCallDiagnosticParams dp = captor.getValue();
+
+ assertNotNull(dp);
+ assertTrue(
+ dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
+ || dp.isTelephonyDumpSysCollectionEnabled());
+
+ //tracking should end
+ assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
+ }
+
+ @Test
+ public void testEmergencyCallWentActiveForLongDuration_shouldNotCollectDiagnostics()
+ throws Exception {
+ Call call = createCall(true, Call.CALL_DIRECTION_OUTGOING);
+ mEmergencyCallDiagnosticLogger.onCallAdded(call);
+
+ //call went active
+ mEmergencyCallDiagnosticLogger.onCallStateChanged(call, CallState.DIALING,
+ CallState.ACTIVE);
+
+ //return large value for time when call is disconnected
+ when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis() + 10000L);
+
+ call.setDisconnectCause(new DisconnectCause(DisconnectCause.ERROR));
+ mEmergencyCallDiagnosticLogger.onCallRemoved(call);
+
+ //no diagnostic data should be persisted
+ verify(mTm, never()).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
+ any());
+
+ //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
new file mode 100644
index 000000000..692d72045
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2022 Tc
+ *
+ * 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 android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+
+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 com.android.server.telecom.Call;
+import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.EmergencyCallHelper;
+import com.android.server.telecom.Timeouts;
+
+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.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";
+ private EmergencyCallHelper mEmergencyCallHelper;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private DefaultDialerCache mDefaultDialerCache;
+ @Mock
+ private Timeouts.Adapter mTimeoutsAdapter;
+ @Mock
+ private UserHandle mUserHandle;
+ @Mock
+ private Call mCall;
+ @Mock private PhoneAccountHandle mPhoneAccountHandle;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ mEmergencyCallHelper = new EmergencyCallHelper(mContext, mDefaultDialerCache,
+ mTimeoutsAdapter);
+ when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYSTEM_DIALER_PACKAGE);
+
+ //start with no perms
+ when(mPackageManager.checkPermission(eq(ACCESS_BACKGROUND_LOCATION),
+ eq(SYSTEM_DIALER_PACKAGE))).thenReturn(
+ PackageManager.PERMISSION_DENIED);
+
+ when(mPackageManager.checkPermission(eq(ACCESS_FINE_LOCATION),
+ eq(SYSTEM_DIALER_PACKAGE))).thenReturn(
+ PackageManager.PERMISSION_DENIED);
+
+ when(mCall.isEmergencyCall()).thenReturn(true);
+ when(mContext.getResources().getBoolean(R.bool.grant_location_permission_enabled)).thenReturn(
+ true);
+ when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class))).thenReturn(
+ 5000L);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private void verifyRevokeInvokedFor(String perm) {
+ verify(mPackageManager, times(1)).revokeRuntimePermission(eq(SYSTEM_DIALER_PACKAGE),
+ eq(perm), eq(mUserHandle));
+ }
+
+ private void verifyRevokeNotInvokedFor(String perm) {
+ verify(mPackageManager, never()).revokeRuntimePermission(eq(SYSTEM_DIALER_PACKAGE),
+ eq(perm), eq(mUserHandle));
+ }
+
+ private void verifyGrantInvokedFor(String perm) {
+ verify(mPackageManager, times(1)).grantRuntimePermission(
+ nullable(String.class),
+ eq(perm), eq(mUserHandle));
+ }
+
+ private void verifyGrantNotInvokedFor(String perm) {
+ verify(mPackageManager, never()).grantRuntimePermission(
+ nullable(String.class),
+ eq(perm), eq(mUserHandle));
+ }
+
+ @SmallTest
+ @Test
+ public void testEmergencyCallHelperRevokesOnlyFinePermAfterBackgroundPermGrantException() {
+
+ //granting of background location perm fails
+ doThrow(new SecurityException()).when(mPackageManager).grantRuntimePermission(
+ nullable(String.class),
+ eq(ACCESS_BACKGROUND_LOCATION), eq(mUserHandle));
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+ //only fine perm should be revoked
+ verifyRevokeNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testEmergencyCallHelperRevokesOnlyBackgroundPermAfterFinePermGrantException() {
+
+ //granting of fine location perm fails
+ doThrow(new SecurityException()).when(mPackageManager).grantRuntimePermission(
+ nullable(String.class),
+ eq(ACCESS_FINE_LOCATION), eq(mUserHandle));
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //only background perm should be revoked
+ verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+ //only fine perm should be revoked
+ verifyRevokeNotInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testNoPermGrantWhenPackageHasAllPerms() {
+
+ when(mPackageManager.checkPermission(eq(ACCESS_BACKGROUND_LOCATION),
+ eq(SYSTEM_DIALER_PACKAGE))).thenReturn(
+ PackageManager.PERMISSION_GRANTED);
+
+ when(mPackageManager.checkPermission(eq(ACCESS_FINE_LOCATION),
+ eq(SYSTEM_DIALER_PACKAGE))).thenReturn(
+ PackageManager.PERMISSION_GRANTED);
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should neither be granted or revoked
+ verifyGrantNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantNotInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testNoPermGrantForNonEmergencyCall() {
+
+ when(mCall.isEmergencyCall()).thenReturn(false);
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should neither be granted or revoked
+ verifyGrantNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantNotInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testNoPermGrantWhenGrantLocationPermissionIsFalse() {
+
+ when(mContext.getResources().getBoolean(R.bool.grant_location_permission_enabled)).thenReturn(
+ false);
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should neither be granted or revoked
+ verifyGrantNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantNotInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnlyFineLocationPermIsGrantedAndRevoked() {
+
+ when(mPackageManager.checkPermission(eq(ACCESS_BACKGROUND_LOCATION),
+ eq(SYSTEM_DIALER_PACKAGE))).thenReturn(
+ PackageManager.PERMISSION_GRANTED);
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should neither be granted or revoked
+ verifyGrantNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testOnlyBackgroundLocationPermIsGrantedAndRevoked() {
+
+ when(mPackageManager.checkPermission(eq(ACCESS_FINE_LOCATION),
+ eq(SYSTEM_DIALER_PACKAGE))).thenReturn(
+ PackageManager.PERMISSION_GRANTED);
+
+ mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+ mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+ //permissions should neither be granted or revoked
+ verifyGrantNotInvokedFor(ACCESS_FINE_LOCATION);
+ verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ verifyRevokeNotInvokedFor(ACCESS_FINE_LOCATION);
+ verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+ }
+
+ @SmallTest
+ @Test
+ public void testIsLastOutgoingEmergencyCallPAH() {
+ PhoneAccountHandle dummyHandle = new PhoneAccountHandle(new ComponentName("pkg", "cls"), "foo");
+ long currentTimeMillis = System.currentTimeMillis();
+ mEmergencyCallHelper.setLastOutgoingEmergencyCallPAH(mPhoneAccountHandle);
+ mEmergencyCallHelper.setLastOutgoingEmergencyCallTimestampMillis(currentTimeMillis);
+
+ // Verify that ECBM is active on mPhoneAccountHandle.
+ assertTrue(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(mPhoneAccountHandle));
+ assertFalse(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(dummyHandle));
+
+ // Expire ECBM and verify that mPhoneAccountHandle is no longer supported for ECBM.
+ mEmergencyCallHelper.setLastOutgoingEmergencyCallTimestampMillis(currentTimeMillis/2);
+ assertFalse(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(mPhoneAccountHandle));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
index 6d15e6041..0bfa987e1 100644
--- a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
+++ b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
@@ -16,20 +16,33 @@
package com.android.server.telecom.tests;
+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 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 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.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;
@@ -38,6 +51,7 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
private static final int TEST_TIMEOUT_MILLIS = 1000;
private HeadsetMediaButton mHeadsetMediaButton;
+ private MediaSession.Callback mSessionCallback;
@Mock private CallsManager mMockCallsManager;
@Mock private HeadsetMediaButton.MediaSessionAdapter mMediaSessionAdapter;
@@ -47,8 +61,15 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
@Before
public void setUp() throws Exception {
super.setUp();
+
+ ArgumentCaptor<MediaSession.Callback> sessionCallbackArgument =
+ ArgumentCaptor.forClass(MediaSession.Callback.class);
+
mHeadsetMediaButton = new HeadsetMediaButton(mContext, mMockCallsManager, mLock,
mMediaSessionAdapter);
+
+ verify(mMediaSessionAdapter).setCallback(sessionCallbackArgument.capture());
+ mSessionCallback = sessionCallbackArgument.getValue();
}
@Override
@@ -61,6 +82,7 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
/**
* Nominal case; just add a call and remove it.
*/
+ @SmallTest
@Test
public void testAddCall() {
Call regularCall = getRegularCall();
@@ -81,6 +103,7 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
/**
* Test a case where a regular call becomes an external call, and back again.
*/
+ @SmallTest
@Test
public void testRegularCallThatBecomesExternal() {
Call regularCall = getRegularCall();
@@ -109,6 +132,85 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
verify(mMediaSessionAdapter).setActive(eq(true));
}
+ @MediumTest
+ @Test
+ public void testExternalCallNotChangesState() {
+ Call externalCall = getRegularCall();
+ when(externalCall.isExternalCall()).thenReturn(true);
+
+ mHeadsetMediaButton.onCallAdded(externalCall);
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+ mHeadsetMediaButton.onCallRemoved(externalCall);
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(false));
+ }
+
+ @SmallTest
+ @Test
+ public void testCallbackReceivesKeyEventUnaware() {
+ mSessionCallback.onMediaButtonEvent(getKeyEventIntent(
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0, false));
+ verify(mMockCallsManager, never()).onMediaButton(anyInt());
+ }
+
+ @SmallTest
+ @Test
+ public void testCallbackReceivesKeyEventShortClick() {
+ mSessionCallback.onMediaButtonEvent(getKeyEventIntent(
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK, false));
+ verify(mMockCallsManager, never()).onMediaButton(anyInt());
+
+ mSessionCallback.onMediaButtonEvent(getKeyEventIntent(
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK, false));
+ verify(mMockCallsManager, times(1)).onMediaButton(HeadsetMediaButton.SHORT_PRESS);
+ }
+
+ @SmallTest
+ @Test
+ public void testCallbackReceivesKeyEventLongClick() {
+ mSessionCallback.onMediaButtonEvent(getKeyEventIntent(
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK, true));
+ verify(mMockCallsManager, times(1)).onMediaButton(HeadsetMediaButton.LONG_PRESS);
+
+ mSessionCallback.onMediaButtonEvent(getKeyEventIntent(
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK, false));
+ verify(mMockCallsManager, times(1)).onMediaButton(HeadsetMediaButton.LONG_PRESS);
+ }
+
+ @SmallTest
+ @Test
+ public void testMediaSessionWrapperSetActive() {
+ MediaSession session = Mockito.mock(MediaSession.class);
+ MediaSessionWrapper wrapper = mHeadsetMediaButton.new MediaSessionWrapper(session);
+
+ final boolean active = true;
+ wrapper.setActive(active);
+ verify(session).setActive(active);
+ }
+
+ @SmallTest
+ @Test
+ public void testMediaSessionWrapperSetCallback() {
+ MediaSession session = Mockito.mock(MediaSession.class);
+ MediaSessionWrapper wrapper = mHeadsetMediaButton.new MediaSessionWrapper(session);
+
+ wrapper.setCallback(mSessionCallback);
+ verify(session).setCallback(mSessionCallback);
+ }
+
+ @SmallTest
+ @Test
+ public void testMediaSessionWrapperIsActive() {
+ MediaSession session = Mockito.mock(MediaSession.class);
+ MediaSessionWrapper wrapper = mHeadsetMediaButton.new MediaSessionWrapper(session);
+
+ final boolean active = true;
+ when(session.isActive()).thenReturn(active);
+ assertEquals(active, wrapper.isActive());
+ }
+
/**
* @return a mock call instance of a regular non-external call.
*/
@@ -117,4 +219,15 @@ public class HeadsetMediaButtonTest extends TelecomTestCase {
when(regularCall.isExternalCall()).thenReturn(false);
return regularCall;
}
+
+ private Intent getKeyEventIntent(int action, int code, boolean longPress) {
+ KeyEvent e = new KeyEvent(action, code);
+ if (longPress) {
+ e = KeyEvent.changeFlags(e, KeyEvent.FLAG_LONG_PRESS);
+ }
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, e);
+ return intent;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 3d387a8ee..683a5e205 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -22,11 +22,13 @@ 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.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;
@@ -34,6 +36,7 @@ import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -52,10 +55,12 @@ import android.app.NotificationManager;
import android.app.UiModeManager;
import android.content.AttributionSource;
import android.content.AttributionSourceState;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.PermissionChecker;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
@@ -63,9 +68,10 @@ import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.compat.testing.PlatformCompatChangeRule;
-import android.os.Binder;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -73,6 +79,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.permission.PermissionCheckerManager;
import android.telecom.CallAudioState;
import android.telecom.InCallService;
@@ -88,6 +95,7 @@ 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.CallsManager;
import com.android.server.telecom.CarModeTracker;
@@ -95,6 +103,7 @@ import com.android.server.telecom.ClockProxy;
import com.android.server.telecom.DefaultDialerCache;
import com.android.server.telecom.EmergencyCallHelper;
import com.android.server.telecom.InCallController;
+import com.android.server.telecom.ParcelableCallUtils;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.R;
import com.android.server.telecom.RoleManagerAdapter;
@@ -112,20 +121,20 @@ import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
-import libcore.junit.util.compat.CoreCompatChangeRule;
-
@RunWith(JUnit4.class)
public class InCallControllerTests extends TelecomTestCase {
@Mock CallsManager mMockCallsManager;
@@ -144,35 +153,42 @@ public class InCallControllerTests extends TelecomTestCase {
@Mock Analytics.CallInfoImpl mCallInfo;
@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
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
- private static final int CURRENT_USER_ID = 900973;
+ private static final int CURRENT_USER_ID = 9;
private static final String DEF_PKG = "defpkg";
private static final String DEF_CLASS = "defcls";
- private static final int DEF_UID = 1;
+ private static final int DEF_UID = 900972;
private static final String SYS_PKG = "syspkg";
private static final String SYS_CLASS = "syscls";
- private static final int SYS_UID = 2;
+ private static final int SYS_UID = 900971;
private static final String COMPANION_PKG = "cpnpkg";
private static final String COMPANION_CLASS = "cpncls";
- private static final int COMPANION_UID = 3;
+ private static final int COMPANION_UID = 900970;
private static final String CAR_PKG = "carpkg";
private static final String CAR2_PKG = "carpkg2";
private static final String CAR_CLASS = "carcls";
private static final String CAR2_CLASS = "carcls";
- private static final int CAR_UID = 4;
- private static final int CAR2_UID = 5;
+ private static final int CAR_UID = 900969;
+ private static final int CAR2_UID = 900968;
private static final String NONUI_PKG = "nonui_pkg";
private static final String NONUI_CLASS = "nonui_cls";
- private static final int NONUI_UID = 6;
+ private static final int NONUI_UID = 900973;
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 PhoneAccountHandle PA_HANDLE =
- new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"), "pa_id");
+ new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"),
+ "pa_id_0", UserHandle.of(CURRENT_USER_ID));
+ private static final UserHandle DUMMY_USER_HANDLE = UserHandle.of(10);
private UserHandle mUserHandle = UserHandle.of(CURRENT_USER_ID);
private InCallController mInCallController;
@@ -180,17 +196,24 @@ public class InCallControllerTests extends TelecomTestCase {
private EmergencyCallHelper mEmergencyCallHelper;
private SystemStateHelper.SystemStateListener mSystemStateListener;
private CarModeTracker mCarModeTracker = spy(new CarModeTracker());
+ private BroadcastReceiver mRegisteredReceiver;
private final int serviceBindingFlags = Context.BIND_AUTO_CREATE
| Context.BIND_FOREGROUND_SERVICE | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
| Context.BIND_SCHEDULE_LIKE_TOP_APP;
+ private UserHandle mChildUserHandle = UserHandle.of(10);
+ private @Mock Call mMockChildUserCall;
+ private UserHandle mParentUserHandle = UserHandle.of(1);
+
@Override
@Before
public void setUp() throws Exception {
super.setUp();
MockitoAnnotations.initMocks(this);
when(mMockCall.getAnalytics()).thenReturn(new Analytics.CallInfo());
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
+ when(mMockCall.getId()).thenReturn("TC@1");
doReturn(mMockResources).when(mMockContext).getResources();
doReturn(mMockAppOpsManager).when(mMockContext).getSystemService(AppOpsManager.class);
doReturn(SYS_PKG).when(mMockResources).getString(
@@ -214,6 +237,12 @@ public class InCallControllerTests extends TelecomTestCase {
mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
mEmergencyCallHelper, mCarModeTracker, mClockProxy);
+ // Capture the broadcast receiver registered.
+ doAnswer(invocation -> {
+ mRegisteredReceiver = invocation.getArgument(0);
+ return null;
+ }).when(mMockContext).registerReceiverAsUser(any(BroadcastReceiver.class),
+ any(), any(IntentFilter.class), any(), any());
ArgumentCaptor<SystemStateHelper.SystemStateListener> systemStateListenerArgumentCaptor
= ArgumentCaptor.forClass(SystemStateHelper.SystemStateListener.class);
@@ -273,6 +302,13 @@ public class InCallControllerTests extends TelecomTestCase {
.thenReturn(PackageManager.PERMISSION_DENIED);
when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0));
+
+ when(mMockContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mMockUserManager);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ // 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);
}
@Override
@@ -285,6 +321,15 @@ public class InCallControllerTests extends TelecomTestCase {
@SmallTest
@Test
+ public void testBringToForeground_NoInCallServices() {
+ // verify that there is not any bound InCallServices for the user requesting for foreground
+ assertFalse(mInCallController.getInCallServices().containsKey(mUserHandle));
+ // ensure that the method behaves properly on invocation
+ mInCallController.bringToForeground(true /* showDialPad */, mUserHandle /* callingUser */);
+ }
+
+ @SmallTest
+ @Test
public void testCarModeAppRemoval() {
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
@@ -348,6 +393,7 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(true);
when(mMockCall.isExternalCall()).thenReturn(false);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class)))
.thenReturn(300_000L);
@@ -359,7 +405,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -378,6 +424,7 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockCall.isExternalCall()).thenReturn(false);
@@ -393,7 +440,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -415,13 +462,14 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockCall.isExternalCall()).thenReturn(false);
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID))
.thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
- anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
+ anyInt(), eq(mUserHandle))).thenReturn(true);
setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
mInCallController.bindToServices(mMockCall);
@@ -445,7 +493,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -467,7 +515,11 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(true);
when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockCall.isExternalCall()).thenReturn(false);
@@ -475,7 +527,7 @@ public class InCallControllerTests extends TelecomTestCase {
.thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT))).thenReturn(true);
+ eq(mUserHandle))).thenReturn(true);
when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class)))
.thenReturn(300_000L);
@@ -503,7 +555,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -526,6 +578,122 @@ public class InCallControllerTests extends TelecomTestCase {
eq(Manifest.permission.ACCESS_FINE_LOCATION), eq(mUserHandle));
}
+ @MediumTest
+ @Test
+ public void
+ testBindToService_UserAssociatedWithCallIsInQuietMode_EmergCallInCallUi_BindsToPrimaryUser()
+ throws Exception {
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.isIncoming()).thenReturn(true);
+ when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
+
+ mInCallController.bindToServices(mMockCall);
+
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+ Intent bindIntent = bindIntentCaptor.getValue();
+ assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+ }
+
+ @MediumTest
+ @Test
+ public void
+ testBindToService_UserAssociatedWithCallIsInQuietMode_NonEmergCallECBM_BindsToPrimaryUser()
+ throws Exception {
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ when(mMockCall.isInECBM()).thenReturn(true);
+ when(mMockCall.isIncoming()).thenReturn(true);
+ when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
+
+ mInCallController.bindToServices(mMockCall);
+
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+ Intent bindIntent = bindIntentCaptor.getValue();
+ assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+ }
+
+ @MediumTest
+ @Test
+ public void
+ testBindToService_UserAssociatedWithCallSecondary_NonEmergCallECBM_BindsToSecondaryUser()
+ throws Exception {
+ UserHandle newUser = new UserHandle(13);
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(newUser);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ when(mMockCall.isInECBM()).thenReturn(true);
+ when(mMockCall.isIncoming()).thenReturn(true);
+ 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(false);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
+
+ mInCallController.bindToServices(mMockCall);
+
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(newUser));
+ Intent bindIntent = bindIntentCaptor.getValue();
+ assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+ }
+
+ @MediumTest
+ @Test
+ public void
+ testBindToService_UserAssociatedWithCallNotInQuietMode_EmergCallInCallUi_BindsToAssociatedUser()
+ throws Exception {
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ 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);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
+
+ mInCallController.bindToServices(mMockCall);
+
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(DUMMY_USER_HANDLE));
+ Intent bindIntent = bindIntentCaptor.getValue();
+ assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+ }
+
/**
* This test verifies the behavior of Telecom when the system dialer crashes on binding and must
* be restarted. Specifically, it ensures when the system dialer crashes we revoke the runtime
@@ -542,7 +710,11 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(true);
when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockCall.isExternalCall()).thenReturn(false);
@@ -552,7 +724,7 @@ public class InCallControllerTests extends TelecomTestCase {
ArgumentCaptor.forClass(ServiceConnection.class);
when(mMockContext.bindServiceAsUser(any(Intent.class), serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT))).thenReturn(true);
+ eq(mUserHandle))).thenReturn(true);
when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class)))
.thenReturn(300_000L);
@@ -580,7 +752,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -609,7 +781,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify we were re-granted the runtime permission.
verify(mMockPackageManager, times(2)).grantRuntimePermission(eq(SYS_PKG),
@@ -629,6 +801,7 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockCallsManager.getAudioState()).thenReturn(null);
when(mMockCallsManager.canAddCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockCall.isExternalCall()).thenReturn(false);
@@ -663,7 +836,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -695,7 +868,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor2.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
bindIntent = bindIntentCaptor2.getValue();
assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
@@ -709,7 +882,9 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.isExternalCall()).thenReturn(false);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
when(mMockCall.getAnalytics()).thenReturn(mCallInfo);
when(mMockContext.bindServiceAsUser(
@@ -741,8 +916,9 @@ public class InCallControllerTests extends TelecomTestCase {
// verify(mockInCallService).setInCallAdapter(any(IInCallAdapter.class));
serviceConnection.onNullBinding(defDialerComponentName);
- verify(mNotificationManager).notify(eq(NOTIFICATION_TAG),
- eq(IN_CALL_SERVICE_NOTIFICATION_ID), any(Notification.class));
+ verify(mNotificationManager).notifyAsUser(eq(NOTIFICATION_TAG),
+ eq(IN_CALL_SERVICE_NOTIFICATION_ID), any(Notification.class),
+ eq(mUserHandle));
verify(mCallInfo).addInCallService(eq(defDialerComponentName.flattenToShortString()),
anyInt(), anyLong(), eq(true));
@@ -751,10 +927,261 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor2.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
assertEquals(sysDialerComponentName, bindIntentCaptor2.getValue().getComponent());
}
+ @Test
+ public void testBindToService_CarModeUI_Crash() throws Exception {
+ setupMocks(false /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
+
+ // Enable car mode
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
+ mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
+
+ // Now bind; we should only bind to one app.
+ mInCallController.bindToServices(mMockCall);
+
+ // Bind InCallServices
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ serviceConnectionCaptor.capture(),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+
+ // Verify bind car mode ui
+ assertEquals(1, bindIntentCaptor.getAllValues().size());
+ verifyBinding(bindIntentCaptor, 0, CAR_PKG, CAR_CLASS);
+
+ // Emulate a crash in the CarModeUI
+ ServiceConnection serviceConnection = serviceConnectionCaptor.getValue();
+ serviceConnection.onServiceDisconnected(bindIntentCaptor.getValue().getComponent());
+
+ ArgumentCaptor<Intent> bindIntentCaptor2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(2)).bindServiceAsUser(
+ bindIntentCaptor2.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+
+ verifyBinding(bindIntentCaptor2, 1, CAR_PKG, CAR_CLASS);
+ }
+
+ /**
+ * This test verifies the behavior of Telecom when the system dialer crashes on binding and must
+ * be restarted. Specifically, it ensures when the system dialer crashes we revoke the runtime
+ * location permission, and when it restarts we re-grant the permission.
+ * @throws Exception
+ */
+ @MediumTest
+ @Test
+ public void testBindToLateConnectionNonUiIcs() throws Exception {
+ Bundle callExtras = new Bundle();
+ callExtras.putBoolean("whatever", true);
+
+ // Make a basic call and bind to the default dialer.
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCallsManager.isInEmergencyCall()).thenReturn(true);
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+ when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+ when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+ when(mMockCall.isExternalCall()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
+ when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID))
+ .thenReturn(DEF_PKG);
+ ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ when(mMockContext.bindServiceAsUser(any(Intent.class), serviceConnectionCaptor.capture(),
+ eq(serviceBindingFlags),
+ eq(mUserHandle))).thenReturn(true);
+ when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class)))
+ .thenReturn(300_000L);
+
+ // Setup package manager; there is a dialer and disable non-ui ICS
+ when(mMockPackageManager.queryIntentServicesAsUser(
+ any(Intent.class), anyInt(), anyInt())).thenReturn(
+ Arrays.asList(
+ getDefResolveInfo(false /* externalCalls */, false /* selfMgd */),
+ getNonUiResolveinfo(false /* selfManaged */,
+ false /* isEnabled */)
+ )
+ );
+ when(mMockPackageManager
+ .getComponentEnabledSetting(new ComponentName(DEF_PKG, DEF_CLASS)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+ when(mMockPackageManager
+ .getComponentEnabledSetting(new ComponentName(NONUI_PKG, NONUI_CLASS)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+
+ mInCallController.addCall(mMockCall);
+ mInCallController.bindToServices(mMockCall);
+
+ // There will be 4 calls for the various types of ICS.
+ verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
+ any(Intent.class),
+ eq(PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS),
+ eq(CURRENT_USER_ID));
+
+ // Verify bind to the dialer
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+
+ Intent bindIntent = bindIntentCaptor.getValue();
+ assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+ assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
+ assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
+
+ // Setup mocks to enable nonui ICS
+ when(mMockPackageManager.queryIntentServicesAsUser(
+ any(Intent.class), anyInt(), anyInt())).thenReturn(
+ Arrays.asList(
+ getDefResolveInfo(false /* externalCalls */, false /* selfMgd */),
+ getNonUiResolveinfo(false /* selfManaged */,
+ true /* isEnabled */)
+ )
+ );
+ when(mMockPackageManager
+ .getComponentEnabledSetting(new ComponentName(NONUI_PKG, NONUI_CLASS)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+ // Emulate a late enable of the non-ui ICS
+ Intent packageUpdated = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+ packageUpdated.setData(Uri.fromParts("package", NONUI_PKG, null));
+ packageUpdated.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+ new String[] {NONUI_CLASS});
+ packageUpdated.putExtra(Intent.EXTRA_UID, NONUI_UID);
+ mRegisteredReceiver.onReceive(mMockContext, packageUpdated);
+
+ // Now, we expect to auto-rebind to the system dialer (verify 2 times since this is the
+ // second binding).
+ verify(mMockContext, times(2)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+
+ // Unbind!
+ mInCallController.unbindFromServices(UserHandle.of(CURRENT_USER_ID));
+
+ // Make sure we unbound 2 times
+ verify(mMockContext, times(2)).unbindService(any(ServiceConnection.class));
+ }
+
+ /**
+ * Tests a case where InCallController DOES NOT bind to ANY InCallServices when the call is
+ * first added, but then one becomes available after the call starts. This test was originally
+ * added to reproduce a bug which would cause the call id mapper in the InCallController to not
+ * track a newly added call unless something was bound when the call was first added.
+ * @throws Exception
+ */
+ @MediumTest
+ @Test
+ public void testNoInitialBinding() throws Exception {
+ Bundle callExtras = new Bundle();
+ callExtras.putBoolean("whatever", true);
+
+ // Make a basic call
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCallsManager.isInEmergencyCall()).thenReturn(true);
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+ when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+ when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+ when(mMockCall.isExternalCall()).thenReturn(false);
+ when(mMockCall.isSelfManaged()).thenReturn(true);
+ when(mMockCall.visibleToInCallService()).thenReturn(true);
+
+ // Dialer doesn't handle these calls, but non-UI ICS does.
+ when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID))
+ .thenReturn(DEF_PKG);
+ ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ when(mMockContext.bindServiceAsUser(any(Intent.class), serviceConnectionCaptor.capture(),
+ eq(serviceBindingFlags),
+ eq(mUserHandle))).thenReturn(true);
+ when(mTimeoutsAdapter.getEmergencyCallbackWindowMillis(any(ContentResolver.class)))
+ .thenReturn(300_000L);
+
+ // Setup package manager; there is a dialer and disable non-ui ICS
+ when(mMockPackageManager.queryIntentServicesAsUser(
+ any(Intent.class), anyInt(), anyInt())).thenReturn(
+ Arrays.asList(
+ getDefResolveInfo(false /* externalCalls */, false /* selfMgd */),
+ getNonUiResolveinfo(true /* selfManaged */,
+ false /* isEnabled */)
+ )
+ );
+ when(mMockPackageManager
+ .getComponentEnabledSetting(new ComponentName(DEF_PKG, DEF_CLASS)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+ when(mMockPackageManager
+ .getComponentEnabledSetting(new ComponentName(NONUI_PKG, NONUI_CLASS)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+
+ // Add the call.
+ mInCallController.onCallAdded(mMockCall);
+
+ // There will be 4 calls for the various types of ICS; this is normal.
+ verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
+ any(Intent.class),
+ eq(PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS),
+ eq(CURRENT_USER_ID));
+
+ // Verify no bind at this point
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, never()).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+
+ // Setup mocks to enable non-ui ICS
+ when(mMockPackageManager.queryIntentServicesAsUser(
+ any(Intent.class), anyInt(), anyInt())).thenReturn(
+ Arrays.asList(
+ getDefResolveInfo(false /* externalCalls */, false /* selfMgd */),
+ getNonUiResolveinfo(true /* selfManaged */,
+ true /* isEnabled */)
+ )
+ );
+ when(mMockPackageManager
+ .getComponentEnabledSetting(new ComponentName(NONUI_PKG, NONUI_CLASS)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+ // Emulate a late enable of the non-ui ICS
+ Intent packageUpdated = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+ packageUpdated.setData(Uri.fromParts("package", NONUI_PKG, null));
+ packageUpdated.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+ new String[] {NONUI_CLASS});
+ packageUpdated.putExtra(Intent.EXTRA_UID, NONUI_UID);
+ mRegisteredReceiver.onReceive(mMockContext, packageUpdated);
+
+ // Make sure we bound to it.
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(mUserHandle));
+ }
+
/**
* Ensures that the {@link InCallController} will bind to an {@link InCallService} which
* supports external calls.
@@ -785,7 +1212,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -805,6 +1232,7 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(true);
when(mMockCall.isExternalCall()).thenReturn(false);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(nullable(Intent.class),
nullable(ServiceConnection.class), anyInt(), nullable(UserHandle.class)))
@@ -823,7 +1251,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Pretend that the call has gone away.
when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList());
@@ -871,7 +1299,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind car mode ui
assertEquals(1, bindIntentCaptor.getAllValues().size());
verifyBinding(bindIntentCaptor, 0, CAR_PKG, CAR_CLASS);
@@ -899,7 +1327,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind to default package, instead of the invalid car mode ui.
assertEquals(1, bindIntentCaptor.getAllValues().size());
verifyBinding(bindIntentCaptor, 0, DEF_PKG, DEF_CLASS);
@@ -942,7 +1370,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind
assertEquals(2, bindIntentCaptor.getAllValues().size());
@@ -986,7 +1414,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind
assertEquals(1, bindIntentCaptor.getAllValues().size());
@@ -995,8 +1423,10 @@ public class InCallControllerTests extends TelecomTestCase {
verifyBinding(bindIntentCaptor, 0, NONUI_PKG, NONUI_CLASS);
// Verify notification is not sent by NotificationManager
- verify(mNotificationManager, times(0)).notify(eq(InCallController.NOTIFICATION_TAG),
- eq(InCallController.IN_CALL_SERVICE_NOTIFICATION_ID), any());
+ verify(mNotificationManager, times(0)).notifyAsUser(
+ eq(InCallController.NOTIFICATION_TAG),
+ eq(InCallController.IN_CALL_SERVICE_NOTIFICATION_ID), any(),
+ eq(mUserHandle));
}
@MediumTest
@@ -1019,7 +1449,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
assertEquals(1, bindIntentCaptor.getAllValues().size());
verifyBinding(bindIntentCaptor, 0, DEF_PKG, DEF_CLASS);
@@ -1057,7 +1487,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
}
/**
@@ -1088,7 +1518,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind car mode ui
assertEquals(4, bindIntentCaptor.getAllValues().size());
@@ -1124,7 +1554,9 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.isExternalCall()).thenReturn(false);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(nullable(Intent.class),
nullable(ServiceConnection.class), anyInt(), nullable(UserHandle.class)))
@@ -1145,7 +1577,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
CompletableFuture<Boolean> bindTimeout = mInCallController.getBindingFuture();
@@ -1213,7 +1645,7 @@ public class InCallControllerTests extends TelecomTestCase {
any(Intent.class),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Now switch to car mode.
// Enable car mode and enter car mode at default priority.
@@ -1225,7 +1657,7 @@ public class InCallControllerTests extends TelecomTestCase {
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind car mode ui
assertEquals(1, bindIntentCaptor.getAllValues().size());
verifyBinding(bindIntentCaptor, 0, CAR_PKG, CAR_CLASS);
@@ -1251,7 +1683,7 @@ public class InCallControllerTests extends TelecomTestCase {
any(Intent.class),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Now switch to car mode.
// Enable car mode and enter car mode at default priority.
@@ -1268,7 +1700,7 @@ public class InCallControllerTests extends TelecomTestCase {
any(Intent.class),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
ServiceConnection serviceConnection = serviceConnectionCaptor.getValue();
ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS);
@@ -1284,6 +1716,191 @@ public class InCallControllerTests extends TelecomTestCase {
verify(mockInCallService, never()).addCall(any(ParcelableCall.class));
}
+ @Test
+ public void testSanitizeDndExtraFromParcelableCall() throws Exception {
+ setupMocks(false /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
+ when(mMockPackageManager.checkPermission(
+ matches(Manifest.permission.READ_CONTACTS),
+ matches(DEF_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
+
+ when(mMockCall.getExtras()).thenReturn(null);
+ ParcelableCall parcelableCallNullExtras = Mockito.spy(
+ ParcelableCallUtils.toParcelableCall(mMockCall,
+ false /* includevideoProvider */,
+ null /* phoneAccountRegistrar */,
+ false /* supportsExternalCalls */,
+ false /* includeRttCall */,
+ false /* isForSystemDialer */));
+
+ when(parcelableCallNullExtras.getExtras()).thenReturn(null);
+ assertNull(parcelableCallNullExtras.getExtras());
+ when(mInCallServiceInfo.getComponentName())
+ .thenReturn(new ComponentName(DEF_PKG, DEF_CLASS));
+ // ensure sanitizeParcelableCallForService does not hit a NPE when Null extras are provided
+ mInCallController.sanitizeParcelableCallForService(mInCallServiceInfo,
+ parcelableCallNullExtras);
+
+
+ Bundle extras = new Bundle();
+ extras.putBoolean(android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB, true);
+ when(mMockCall.getExtras()).thenReturn(extras);
+
+ ParcelableCall parcelableCallWithExtras = ParcelableCallUtils.toParcelableCall(mMockCall,
+ false /* includevideoProvider */,
+ null /* phoneAccountRegistrar */,
+ false /* supportsExternalCalls */,
+ false /* includeRttCall */,
+ false /* isForSystemDialer */);
+
+ // ensure sanitizeParcelableCallForService sanitizes the
+ // EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB from a ParcelableCall
+ // w/o Manifest.permission.READ_CONTACTS
+ ParcelableCall sanitizedCall =
+ mInCallController.sanitizeParcelableCallForService(mInCallServiceInfo,
+ parcelableCallWithExtras);
+
+ // sanitized call should not have the extra
+ assertFalse(sanitizedCall.getExtras().containsKey(
+ android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB));
+
+ // root ParcelableCall should still have the extra
+ assertTrue(parcelableCallWithExtras.getExtras().containsKey(
+ android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB));
+ }
+
+ @Test
+ public void testSecondaryUserCallBindToCurrentUser() throws Exception {
+ setupMocks(true /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ // 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);
+
+ mInCallController.bindToServices(mMockCall);
+
+ // Bind InCallService on UserHandle.CURRENT and not the user from the call (mUserHandle)
+ ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockContext, times(1)).bindServiceAsUser(
+ bindIntentCaptor.capture(),
+ any(ServiceConnection.class),
+ eq(serviceBindingFlags),
+ eq(UserHandle.CURRENT));
+ }
+
+ @Test
+ public void testGetUserFromCall_TargetPhoneAccountNotSet() throws Exception {
+ setupMocks(false /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ UserHandle testUser = new UserHandle(10);
+
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(null);
+ when(mMockCall.getAssociatedUser()).thenReturn(testUser);
+
+ // Bind to ICS. The mapping should've been inserted with the testUser as the key.
+ mInCallController.bindToServices(mMockCall);
+ assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
+
+ // Set the target phone account. Simulates the flow when the user has chosen which sim to
+ // place the call on.
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+
+ // Remove the call. This invokes getUserFromCall to remove the ICS mapping.
+ when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList());
+ mInCallController.onCallRemoved(mMockCall);
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+ // Verify that the mapping was properly removed.
+ assertNull(mInCallController.getInCallServiceConnections().get(testUser));
+ }
+
+ @Test
+ public void testGetUserFromCall_IncomingCall() throws Exception {
+ setupMocks(false /* isExternalCall */);
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ // Explicitly test on a different user to avoid interference with current user.
+ UserHandle testUser = new UserHandle(10);
+
+ // Set user handle in target phone account to test user
+ when(mMockCall.getAssociatedUser()).thenReturn(testUser);
+ when(mMockCall.isIncoming()).thenReturn(true);
+
+ // Bind to ICS. The mapping should've been inserted with the testUser as the key.
+ mInCallController.bindToServices(mMockCall);
+ assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
+
+ // Remove the call. This invokes getUserFromCall to remove the ICS mapping.
+ when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList());
+ mInCallController.onCallRemoved(mMockCall);
+ waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+ // Verify that the mapping was properly removed.
+ assertNull(mInCallController.getInCallServiceConnections().get(testUser));
+ }
+
+ private void setupMocksForWorkProfileTest() {
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
+ when(mMockChildUserCall.isIncoming()).thenReturn(false);
+ when(mMockChildUserCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+ when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
+ when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
+ anyInt(), any())).thenReturn(true);
+ when(mMockChildUserCall.isExternalCall()).thenReturn(false);
+ when(mMockChildUserCall.isSelfManaged()).thenReturn(true);
+ 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(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);
+ }
+
+ @Test
+ public void testManagedProfileCallQueriesIcsUsingParentUserToo() throws Exception {
+ setupMocksForWorkProfileTest();
+ setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+ setupMockPackageManager(true /* default */,
+ true /*useNonUiInCalls*/, true /*useAppOpNonUiInCalls*/,
+ true /*useSystemDialer*/, false /*includeExternalCalls*/,
+ true /*includeSelfManagedCallsInDefaultDialer*/,
+ true /*includeSelfManagedCallsInCarModeDialer*/,
+ true /*includeSelfManagedCallsInNonUi*/);
+
+ //pass in call by child/work-profileuser
+ mInCallController.bindToServices(mMockChildUserCall);
+
+ // Verify that queryIntentServicesAsUser is also called with parent handle
+ // Query for the different InCallServices
+ ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ ArgumentCaptor<Integer> flagCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockPackageManager, times(6)).queryIntentServicesAsUser(
+ queryIntentCaptor.capture(), flagCaptor.capture(), userIdCaptor.capture());
+ List<Integer> userIds = userIdCaptor.getAllValues();
+
+ //check if queryIntentServices was called with child user handle
+ assertTrue("no query parent user handle",
+ userIds.contains(mChildUserHandle.getIdentifier()));
+ //check if queryIntentServices was also called with parent user handle
+ assertTrue("no query parent user handle",
+ userIds.contains(mParentUserHandle.getIdentifier()));
+ }
+
private void setupMocks(boolean isExternalCall) {
setupMocks(isExternalCall, false /* isSelfManagedCall */);
}
@@ -1293,10 +1910,11 @@ public class InCallControllerTests extends TelecomTestCase {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
+ when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
- anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
+ anyInt(), any(UserHandle.class))).thenReturn(true);
when(mMockCall.isExternalCall()).thenReturn(isExternalCall);
when(mMockCall.isSelfManaged()).thenReturn(isSelfManagedCall);
when(mMockCall.visibleToInCallService()).thenReturn(isSelfManagedCall);
@@ -1378,14 +1996,14 @@ public class InCallControllerTests extends TelecomTestCase {
}};
}
- private ResolveInfo getNonUiResolveinfo(boolean supportsSelfManaged) {
+ private ResolveInfo getNonUiResolveinfo(boolean supportsSelfManaged, boolean isEnabled) {
return new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = NONUI_PKG;
serviceInfo.name = NONUI_CLASS;
serviceInfo.applicationInfo = new ApplicationInfo();
serviceInfo.applicationInfo.uid = NONUI_UID;
- serviceInfo.enabled = true;
+ serviceInfo.enabled = isEnabled;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
serviceInfo.metaData = new Bundle();
if (supportsSelfManaged) {
@@ -1476,7 +2094,7 @@ public class InCallControllerTests extends TelecomTestCase {
} else {
// InCallController uses a blank package name when querying for non-ui incalls
if (useNonUiInCalls) {
- resolveInfo.add(getNonUiResolveinfo(includeSelfManagedCallsInNonUi));
+ resolveInfo.add(getNonUiResolveinfo(includeSelfManagedCallsInNonUi, true));
}
// InCallController uses a blank package name when querying for App Op non-ui incalls
if (useAppOpNonUiInCalls) {
@@ -1487,7 +2105,7 @@ public class InCallControllerTests extends TelecomTestCase {
return resolveInfo;
}
}).when(mMockPackageManager).queryIntentServicesAsUser(
- any(Intent.class), anyInt(), eq(CURRENT_USER_ID));
+ any(Intent.class), anyInt(), anyInt());
if (useDefaultDialer) {
when(mMockPackageManager
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index d114cb8ca..88b5bb52a 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -26,9 +26,11 @@ import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
import android.telecom.ParcelableCall;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -105,6 +107,15 @@ public class InCallServiceFixture implements TestFixture<IInCallService> {
}
@Override
+ public void onCallEndpointChanged(CallEndpoint callEndpoint) {}
+
+ @Override
+ public void onAvailableCallEndpointsChanged(List<CallEndpoint> availableCallEndpoints) {}
+
+ @Override
+ public void onMuteStateChanged(boolean isMuted) {}
+
+ @Override
public void bringToForeground(boolean showDialpad) throws RemoteException {
mBringToForeground = true;
mShowDialpad = showDialpad;
diff --git a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
index eadda0d09..f11afc1e2 100644
--- a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
@@ -140,13 +140,16 @@ public class InCallTonePlayerTest extends TelecomTestCase {
@SmallTest
@Test
- public void testNoEndCallToneInSilence() {
+ public void testEndCallTonePlaysWhenRingIsSilent() {
when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(false);
- assertFalse(mInCallTonePlayer.startTone());
+ assertTrue(mInCallTonePlayer.startTone());
+ // Verify we did play a tone.
+ verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
+ verify(mCallAudioManager).setIsTonePlaying(eq(true));
- // Verify we didn't play a tone.
- verify(mCallAudioManager, never()).setIsTonePlaying(eq(true));
- verify(mMediaPlayerFactory, never()).get(anyInt(), any());
+ mInCallTonePlayer.stopTone();
+ // Timeouts due to threads!
+ verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
}
@SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
index a871b734f..914fdc5ba 100644
--- a/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
@@ -17,9 +17,12 @@
package com.android.server.telecom.tests;
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;
@@ -72,6 +75,8 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
when(mAudioCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
when(mAudioCall.getTargetPhoneAccountLabel()).thenReturn("Bar");
+ when(mAudioCall.getAssociatedUser()).
+ thenReturn(UserHandle.CURRENT);
when(mVideoCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
when(mVideoCall.getTargetPhoneAccountLabel()).thenReturn("Bar");
when(mRingingCall.isSelfManaged()).thenReturn(true);
@@ -79,6 +84,8 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
when(mRingingCall.getState()).thenReturn(CallState.RINGING);
when(mRingingCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
when(mRingingCall.getTargetPhoneAccountLabel()).thenReturn("Foo");
+ when(mRingingCall.getAssociatedUser()).
+ thenReturn(UserHandle.CURRENT);
when(mRingingCall.getHandoverState()).thenReturn(HandoverState.HANDOVER_NONE);
}
@@ -95,8 +102,10 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
@Test
public void testSingleCall() {
mIncomingCallNotifier.onCallAdded(mAudioCall);
- verify(mNotificationManager, never()).notify(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any());
+ verify(mNotificationManager, never()).notifyAsUser(
+ eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any(),
+ eq(UserHandle.CURRENT));
}
/**
@@ -107,8 +116,10 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
public void testIncomingDuringOngoingCall() {
when(mCallsManagerProxy.hasUnholdableCallsForOtherConnectionService(any())).thenReturn(false);
mIncomingCallNotifier.onCallAdded(mRingingCall);
- verify(mNotificationManager, never()).notify(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any());
+ verify(mNotificationManager, never()).notifyAsUser(
+ eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any(),
+ eq(UserHandle.CURRENT));
}
/**
@@ -123,8 +134,10 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
mIncomingCallNotifier.onCallAdded(mAudioCall);
mIncomingCallNotifier.onCallAdded(mRingingCall);
- verify(mNotificationManager, never()).notify(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any());;
+ verify(mNotificationManager, never()).notifyAsUser(
+ eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any(),
+ eq(UserHandle.CURRENT));
}
/**
@@ -139,11 +152,13 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
mIncomingCallNotifier.onCallAdded(mAudioCall);
mIncomingCallNotifier.onCallAdded(mRingingCall);
- verify(mNotificationManager).notify(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any());
+ verify(mNotificationManager).notifyAsUser(
+ eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any(),
+ eq(UserHandle.CURRENT));
mIncomingCallNotifier.onCallRemoved(mRingingCall);
- verify(mNotificationManager).cancel(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL));
+ verify(mNotificationManager).cancelAsUser(eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), eq(UserHandle.CURRENT));
}
/**
@@ -161,8 +176,10 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
mIncomingCallNotifier.onCallAdded(mRingingCall);
// Incoming call is in the middle of a handover, don't expect to be notified.
- verify(mNotificationManager, never()).notify(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any());;
+ verify(mNotificationManager, never()).notifyAsUser(
+ eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any(),
+ eq(UserHandle.CURRENT));
}
/**
@@ -180,7 +197,9 @@ public class IncomingCallNotifierTest extends TelecomTestCase {
mIncomingCallNotifier.onCallAdded(mRingingCall);
// Incoming call is done a handover, don't expect to be notified.
- verify(mNotificationManager, never()).notify(eq(IncomingCallNotifier.NOTIFICATION_TAG),
- eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any());;
+ verify(mNotificationManager, never()).notifyAsUser(
+ eq(IncomingCallNotifier.NOTIFICATION_TAG),
+ eq(IncomingCallNotifier.NOTIFICATION_INCOMING_CALL), any(),
+ eq(UserHandle.CURRENT));
}
}
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
new file mode 100644
index 000000000..e4418357f
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.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.tests;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.telecom.PhoneAccountHandle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.telecom.CallerInfo;
+
+import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.MissedCallNotifier.CallInfo;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+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 =
+ new ComponentName("com.anything", "com.whatever");
+ private static final Uri TEL_CALL_HANDLE = Uri.parse("tel:+11915552620");
+ private static final long CALL_TIMESTAMP = 1;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testCallInfoFactory() {
+ final CallerInfo callerInfo = new CallerInfo();
+ final String phoneNumber = "1111";
+ final String name = "name";
+ callerInfo.setPhoneNumber(phoneNumber);
+ callerInfo.setName(name);
+ final PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(COMPONENT_NAME, "id");
+
+ MissedCallNotifier.CallInfo callInfo = new MissedCallNotifier.CallInfoFactory()
+ .makeCallInfo(callerInfo, phoneAccountHandle, TEL_CALL_HANDLE, CALL_TIMESTAMP);
+
+ assertEquals(callerInfo, callInfo.getCallerInfo());
+ assertEquals(phoneAccountHandle, callInfo.getPhoneAccountHandle());
+ assertEquals(TEL_CALL_HANDLE, callInfo.getHandle());
+ assertEquals(TEL_CALL_HANDLE.getSchemeSpecificPart(),
+ callInfo.getHandleSchemeSpecificPart());
+ assertEquals(CALL_TIMESTAMP, callInfo.getCreationTimeMillis());
+ assertEquals(phoneNumber, callInfo.getPhoneNumber());
+ assertEquals(name, callInfo.getName());
+ }
+
+ @SmallTest
+ @Test
+ public void testCallInfoFactoryNullParam() {
+ MissedCallNotifier.CallInfo callInfo = new MissedCallNotifier.CallInfoFactory()
+ .makeCallInfo(null, null, null, CALL_TIMESTAMP);
+
+ assertNull(callInfo.getCallerInfo());
+ assertNull(callInfo.getPhoneAccountHandle());
+ assertNull(callInfo.getHandle());
+ assertNull(callInfo.getHandleSchemeSpecificPart());
+ assertEquals(CALL_TIMESTAMP, callInfo.getCreationTimeMillis());
+ assertNull(callInfo.getPhoneNumber());
+ assertNull(callInfo.getName());
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
index f2f0cd899..4af3de343 100644
--- a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
@@ -16,6 +16,7 @@
package com.android.server.telecom.tests;
+import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
@@ -117,6 +118,8 @@ public class MissedInformationTest extends TelecomSystemTest {
mPackageManager = mContext.getPackageManager();
when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
mCountDownLatch = new CountDownLatch(1);
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingPermission(MODIFY_PHONE_STATE);
}
@Override
@@ -147,6 +150,8 @@ public class MissedInformationTest extends TelecomSystemTest {
public void testEmergencyCallPlacing() throws Exception {
Analytics.dumpToParcelableAnalytics();
setUpEmergencyCall();
+ when(mEmergencyCall.getAssociatedUser()).
+ thenReturn(mPhoneAccountA0.getAccountHandle().getUserHandle());
mCallsManager.addCall(mEmergencyCall);
assertTrue(mCallsManager.isInEmergencyCall());
@@ -354,6 +359,9 @@ public class MissedInformationTest extends TelecomSystemTest {
doReturn(mNotificationManager).when(mSpyContext)
.getSystemService(Context.NOTIFICATION_SERVICE);
doReturn(false).when(mNotificationManager).matchesCallFilter(any(Bundle.class));
+ doReturn(false).when(mIncomingCall).wasDndCheckComputedForCall();
+ mCallsManager.getRinger().setNotificationManager(mNotificationManager);
+
CallFilteringResult result = new CallFilteringResult.Builder()
.setShouldAllowCall(true)
.build();
diff --git a/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
new file mode 100644
index 000000000..ed74637d3
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.MmiUtils;
+
+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 MmiUtilsTest extends TelecomTestCase {
+
+ private static final String[] sDangerousDialStrings = {
+ "*21*1234567#", // fwd unconditionally to 1234567,
+ "*67*1234567#", // fwd to 1234567 when line is busy
+ "*61*1234567#", // fwd to 1234567 when no one picks up
+ "*62*1234567#", // fwd to 1234567 when out of range
+ "*004*1234567#", // fwd to 1234567 when busy, not pickup up, out of range
+ "*004*1234567#", // fwd to 1234567 conditionally
+ "**21*1234567#", // fwd unconditionally to 1234567
+
+ // north american vertical service codes
+
+ "*094565678", // Selective Call Blocking/Reporting
+ "*4278889", // Change Forward-To Number for Customer Programmable Call Forwarding Don't
+ // Answer
+ "*5644456", // Change Forward-To Number for ISDN Call Forwarding
+ "*6045677", // Selective Call Rejection Activation
+ "*635678", // Selective Call Forwarding Activation
+ "*64678899", // Selective Call Acceptance Activation
+ "*683456", // Call Forwarding Busy Line/Don't Answer Activation
+ "*721234", // Call Forwarding Activation
+ "*77", // Anonymous Call Rejection Activation
+ "*78", // Do Not Disturb Activation
+ };
+
+ private MmiUtils mMmiUtils = new MmiUtils();
+ private static final String[] sNonDangerousDialStrings = {"*6712345678", "*272", "*272911"};
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testDangerousDialStringsDetected() throws Exception {
+ for (String s : sDangerousDialStrings) {
+ Uri.Builder b = new Uri.Builder();
+ b.scheme("tel").opaquePart(s);
+ assertTrue(mMmiUtils.isDangerousMmiOrVerticalCode(b.build()));
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void testNonDangerousDialStringsNotDetected() throws Exception {
+ for (String s : sNonDangerousDialStrings) {
+ Uri.Builder b = new Uri.Builder();
+ b.scheme("tel").opaquePart(s);
+ assertFalse(mMmiUtils.isDangerousMmiOrVerticalCode(b.build()));
+ }
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index 2614abf2f..33acd9811 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -56,6 +56,7 @@ import android.test.suitebuilder.annotation.SmallTest;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.MmiUtils;
import com.android.server.telecom.NewOutgoingCallIntentBroadcaster;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
@@ -93,13 +94,14 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
@Mock private RoleManagerAdapter mRoleManagerAdapter;
@Mock private DefaultDialerCache mDefaultDialerCache;
+ @Mock private MmiUtils mMmiUtils;
private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl();
@Override
@Before
public void setUp() throws Exception {
super.setUp();
- when(mCall.getInitiatingUser()).thenReturn(UserHandle.CURRENT);
+ when(mCall.getAssociatedUser()).thenReturn(UserHandle.CURRENT);
when(mCallsManager.getLock()).thenReturn(new TelecomSystem.SyncRoot() { });
when(mCallsManager.getSystemStateHelper()).thenReturn(mSystemStateHelper);
when(mCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
@@ -232,7 +234,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
public void testEmergencyCallWithNonDefaultDialer() {
Uri handle = Uri.parse("tel:6505551911");
doReturn(true).when(mComponentContextFixture.getTelephonyManager())
- .isPotentialEmergencyNumber(eq(handle.getSchemeSpecificPart()));
+ .isEmergencyNumber(eq(handle.getSchemeSpecificPart()));
Intent intent = new Intent(Intent.ACTION_CALL, handle);
String ui_package_string = "sample_string_1";
@@ -261,6 +263,58 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, dialerIntent.getFlags());
}
+ @Test
+ public void testDangerousMmiCodeWithNonDefaultDialer() {
+ Uri handle = Uri.parse("tel:*21*1234567#");
+ doReturn(true).when(mMmiUtils).isDangerousMmiOrVerticalCode(handle);
+ Intent intent = new Intent(Intent.ACTION_CALL, handle);
+
+ String ui_package_string = "sample_string_1";
+ String dialer_default_class_string = "sample_string_2";
+ mComponentContextFixture.putResource(com.android.internal.R.string.config_defaultDialer,
+ ui_package_string);
+ mComponentContextFixture.putResource(R.string.dialer_default_class,
+ dialer_default_class_string);
+ when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(ui_package_string);
+ when(mDefaultDialerCache.getDialtactsSystemDialerComponent()).thenReturn(
+ new ComponentName(ui_package_string, dialer_default_class_string));
+
+ int result = processIntent(intent, false).disconnectCause;
+
+ assertEquals(DisconnectCause.OUTGOING_CANCELED, result);
+ verifyNoBroadcastSent();
+ verifyNoCallPlaced();
+
+ ArgumentCaptor<Intent> dialerIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext).startActivityAsUser(dialerIntentCaptor.capture(), any(UserHandle.class));
+ Intent dialerIntent = dialerIntentCaptor.getValue();
+ assertEquals(new ComponentName(ui_package_string, dialer_default_class_string),
+ dialerIntent.getComponent());
+ assertEquals(Intent.ACTION_DIAL, dialerIntent.getAction());
+ assertEquals(handle, dialerIntent.getData());
+ assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, dialerIntent.getFlags());
+ }
+
+ @Test
+ public void testNonDangerousMmiCodeWithNonDefaultDialer() {
+ Uri handle = Uri.parse("tel:*12*1234567#");
+ doReturn(false).when(mMmiUtils).isDangerousMmiOrVerticalCode(handle);
+ Intent intent = new Intent(Intent.ACTION_CALL, handle);
+
+ String ui_package_string = "sample_string_1";
+ String dialer_default_class_string = "sample_string_2";
+ mComponentContextFixture.putResource(com.android.internal.R.string.config_defaultDialer,
+ ui_package_string);
+ mComponentContextFixture.putResource(R.string.dialer_default_class,
+ dialer_default_class_string);
+ when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(ui_package_string);
+ when(mDefaultDialerCache.getDialtactsSystemDialerComponent()).thenReturn(
+ new ComponentName(ui_package_string, dialer_default_class_string));
+
+ int result = processIntent(intent, false).disconnectCause;
+ assertEquals(DisconnectCause.NOT_DISCONNECTED, result);
+ }
+
@SmallTest
@Test
public void testActionCallEmergencyCall() {
@@ -303,7 +357,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
public void testActionEmergencyWithNonEmergencyNumber() {
Uri handle = Uri.parse("tel:6505551911");
doReturn(false).when(mComponentContextFixture.getTelephonyManager())
- .isPotentialEmergencyNumber(eq(handle.getSchemeSpecificPart()));
+ .isEmergencyNumber(eq(handle.getSchemeSpecificPart()));
Intent intent = new Intent(Intent.ACTION_CALL_EMERGENCY, handle);
int result = processIntent(intent, true).disconnectCause;
@@ -317,7 +371,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
int videoState = VideoProfile.STATE_BIDIRECTIONAL;
boolean isSpeakerphoneOn = true;
doReturn(true).when(mComponentContextFixture.getTelephonyManager())
- .isPotentialEmergencyNumber(eq(handle.getSchemeSpecificPart()));
+ .isEmergencyNumber(eq(handle.getSchemeSpecificPart()));
intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isSpeakerphoneOn);
intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
@@ -439,7 +493,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
result.receiver.setResultData(newEmergencyNumber);
doReturn(true).when(mComponentContextFixture.getTelephonyManager())
- .isPotentialEmergencyNumber(eq(newEmergencyNumber));
+ .isEmergencyNumber(eq(newEmergencyNumber));
result.receiver.onReceive(mContext, result.intent);
verify(mCall).disconnect(eq(0L));
}
@@ -488,7 +542,7 @@ public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
boolean isDefaultPhoneApp) {
NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster(
mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
- isDefaultPhoneApp, mDefaultDialerCache);
+ isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils);
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 a50328386..fed80847f 100644
--- a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
@@ -87,7 +87,7 @@ public class ParcelableCallUtilsTest extends TelecomTestCase {
@SmallTest
@Test
public void testParcelForNonSystemDialer() {
- mCall.putExtras(Call.SOURCE_CONNECTION_SERVICE, getSomeExtras());
+ mCall.putConnectionServiceExtras(getSomeExtras());
ParcelableCall call = ParcelableCallUtils.toParcelableCall(mCall,
false /* includevideoProvider */,
null /* phoneAccountRegistrar */,
@@ -105,7 +105,7 @@ public class ParcelableCallUtilsTest extends TelecomTestCase {
@SmallTest
@Test
public void testParcelForSystemDialer() {
- mCall.putExtras(Call.SOURCE_CONNECTION_SERVICE, getSomeExtras());
+ mCall.putConnectionServiceExtras(getSomeExtras());
ParcelableCall call = ParcelableCallUtils.toParcelableCall(mCall,
false /* includevideoProvider */,
null /* phoneAccountRegistrar */,
@@ -123,7 +123,7 @@ public class ParcelableCallUtilsTest extends TelecomTestCase {
@SmallTest
@Test
public void testParcelForSystemCallScreening() {
- mCall.putExtras(Call.SOURCE_CONNECTION_SERVICE, getSomeExtras());
+ mCall.putConnectionServiceExtras(getSomeExtras());
ParcelableCall call = ParcelableCallUtils.toParcelableCallForScreening(mCall,
true /* isPartOfSystemDialer */);
@@ -137,7 +137,7 @@ public class ParcelableCallUtilsTest extends TelecomTestCase {
@SmallTest
@Test
public void testParcelForSystemNonSystemCallScreening() {
- mCall.putExtras(Call.SOURCE_CONNECTION_SERVICE, getSomeExtras());
+ mCall.putConnectionServiceExtras(getSomeExtras());
ParcelableCall call = ParcelableCallUtils.toParcelableCallForScreening(mCall,
false /* isPartOfSystemDialer */);
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index ffa08e2b9..e573bb81f 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -22,11 +22,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 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.Mockito.clearInvocations;
+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;
@@ -83,22 +87,30 @@ import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
@RunWith(JUnit4.class)
public class PhoneAccountRegistrarTest extends TelecomTestCase {
private static final int MAX_VERSION = Integer.MAX_VALUE;
+ private static final int INVALID_CHAR_LIMIT_COUNT =
+ PhoneAccountRegistrar.MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT + 1;
+ private static final String INVALID_STR = "a".repeat(INVALID_CHAR_LIMIT_COUNT);
private static final String FILE_NAME = "phone-account-registrar-test-1223.xml";
private static final String TEST_LABEL = "right";
+ private static final String TEST_ID = "123";
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 TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
private PhoneAccountRegistrar mRegistrar;
@Mock private SubscriptionManager mSubscriptionManager;
@@ -163,7 +175,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
testBundle.putString("EXTRA_STR1", "Hello");
testBundle.putString("EXTRA_STR2", "There");
- PhoneAccount input = makeQuickAccountBuilder("id0", 0)
+ PhoneAccount input = makeQuickAccountBuilder("id0", 0, null)
.addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
.addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
.setExtras(testBundle)
@@ -271,7 +283,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
// Put in something valid so the bundle exists.
testBundle.putString("EXTRA_OK", "OK");
- PhoneAccount input = makeQuickAccountBuilder("id0", 0)
+ PhoneAccount input = makeQuickAccountBuilder("id0", 0, null)
.addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
.addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
.setExtras(testBundle)
@@ -309,24 +321,33 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
Mockito.mock(IConnectionService.class));
- registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++)
+ registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++, null)
.setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
| PhoneAccount.CAPABILITY_CALL_PROVIDER)
.build());
- registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++)
+ registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++, null)
.setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
| PhoneAccount.CAPABILITY_CALL_PROVIDER)
.build());
- registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++)
+ registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++, null)
.setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
| PhoneAccount.CAPABILITY_CALL_PROVIDER)
.build());
- registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++)
+ registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++, null)
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+ .build());
+ registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++, USER_HANDLE_10)
+ .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+ | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .build());
+ registerAndEnableAccount(makeQuickAccountBuilder("id" + i, i++, USER_HANDLE_10)
.setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
.build());
- assertEquals(4, mRegistrar.getAllPhoneAccountsOfCurrentUser().size());
- assertEquals(3, mRegistrar.getCallCapablePhoneAccountsOfCurrentUser(null, false).size());
+ assertEquals(6, mRegistrar.
+ getAllPhoneAccounts(null, true).size());
+ assertEquals(4, mRegistrar.getCallCapablePhoneAccounts(null, false,
+ null, true).size());
assertEquals(null, mRegistrar.getSimCallManagerOfCurrentUser());
assertEquals(null, mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
PhoneAccount.SCHEME_TEL));
@@ -674,6 +695,75 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
assertEquals(TEST_LABEL, registeredAccount.getLabel());
}
+ @MediumTest
+ @Test
+ public void testSecurityExceptionIsThrownWhenSelfManagedLacksPermissions() {
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName("self", "managed"), "selfie1");
+
+ PhoneAccount accountWithoutCapability = new PhoneAccount.Builder(handle, "label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .build();
+
+ assertFalse(mRegistrar.hasTransactionalCallCapabilities(accountWithoutCapability));
+
+ try {
+ mRegistrar.registerPhoneAccount(accountWithoutCapability);
+ fail("should not be able to register account");
+ } catch (SecurityException securityException) {
+ // test pass
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ @MediumTest
+ @Test
+ public void testSelfManagedPhoneAccountWithTransactionalOperations() {
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName("self", "managed"), "selfie1");
+
+ PhoneAccount accountWithCapability = new PhoneAccount.Builder(handle, "label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ .build();
+
+ assertTrue(mRegistrar.hasTransactionalCallCapabilities(accountWithCapability));
+
+ try {
+ mRegistrar.registerPhoneAccount(accountWithCapability);
+ PhoneAccount registeredAccount = mRegistrar.getPhoneAccountUnchecked(handle);
+ assertEquals(TEST_LABEL, registeredAccount.getLabel().toString());
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ @MediumTest
+ @Test
+ public void testRegisterPhoneAccountAmendsSelfManagedCapabilityInternally() {
+ // GIVEN
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName("self", "managed"), "selfie1");
+ PhoneAccount accountWithCapability = new PhoneAccount.Builder(handle, "label")
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ .build();
+
+ assertTrue(mRegistrar.hasTransactionalCallCapabilities(accountWithCapability));
+
+ try {
+ // WHEN
+ mRegistrar.registerPhoneAccount(accountWithCapability);
+ PhoneAccount registeredAccount = mRegistrar.getPhoneAccountUnchecked(handle);
+ // THEN
+ assertEquals(PhoneAccount.CAPABILITY_SELF_MANAGED, (registeredAccount.getCapabilities()
+ & PhoneAccount.CAPABILITY_SELF_MANAGED));
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
/**
* Tests to ensure that when registering a self-managed PhoneAccount, it cannot also be defined
* as a call provider, connection manager, or sim subscription.
@@ -727,7 +817,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
registerAndEnableAccount(nonSimAccount);
registerAndEnableAccount(simAccount);
- List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle());
+ List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle(), false);
assertTrue(accounts.get(0).getLabel().toString().equals("2"));
assertTrue(accounts.get(1).getLabel().toString().equals("1"));
}
@@ -770,7 +860,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
registerAndEnableAccount(account2);
registerAndEnableAccount(account1);
- List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle());
+ List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle(), false);
assertTrue(accounts.get(0).getLabel().toString().equals("c"));
assertTrue(accounts.get(1).getLabel().toString().equals("b"));
assertTrue(accounts.get(2).getLabel().toString().equals("a"));
@@ -808,7 +898,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
registerAndEnableAccount(account2);
registerAndEnableAccount(account3);
- List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle());
+ List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle(), false);
assertTrue(accounts.get(0).getLabel().toString().equals("a"));
assertTrue(accounts.get(1).getLabel().toString().equals("b"));
assertTrue(accounts.get(2).getLabel().toString().equals("c"));
@@ -886,7 +976,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
registerAndEnableAccount(account5);
registerAndEnableAccount(account6);
- List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle());
+ List<PhoneAccount> accounts = mRegistrar.getAllPhoneAccounts(Process.myUserHandle(), false);
// Sim accts ordered by sort order first
assertTrue(accounts.get(0).getLabel().toString().equals("z"));
assertTrue(accounts.get(1).getLabel().toString().equals("y"));
@@ -910,14 +1000,14 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
public void testGetByEnabledState() throws Exception {
mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
Mockito.mock(IConnectionService.class));
- mRegistrar.registerPhoneAccount(makeQuickAccountBuilder("id1", 1)
+ mRegistrar.registerPhoneAccount(makeQuickAccountBuilder("id1", 1, null)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.build());
assertEquals(0, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_TEL,
- false /* includeDisabled */, Process.myUserHandle()).size());
+ false /* includeDisabled */, Process.myUserHandle(), false).size());
assertEquals(1, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_TEL,
- true /* includeDisabled */, Process.myUserHandle()).size());
+ true /* includeDisabled */, Process.myUserHandle(), false).size());
}
/**
@@ -930,21 +1020,21 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
public void testGetByScheme() throws Exception {
mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
Mockito.mock(IConnectionService.class));
- registerAndEnableAccount(makeQuickAccountBuilder("id1", 1)
+ registerAndEnableAccount(makeQuickAccountBuilder("id1", 1, null)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_SIP))
.build());
- registerAndEnableAccount(makeQuickAccountBuilder("id2", 2)
+ registerAndEnableAccount(makeQuickAccountBuilder("id2", 2, null)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
.build());
assertEquals(1, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_SIP,
- false /* includeDisabled */, Process.myUserHandle()).size());
+ false /* includeDisabled */, Process.myUserHandle(), false).size());
assertEquals(1, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_TEL,
- false /* includeDisabled */, Process.myUserHandle()).size());
+ false /* includeDisabled */, Process.myUserHandle(), false).size());
assertEquals(2, mRegistrar.getCallCapablePhoneAccounts(null, false /* includeDisabled */,
- Process.myUserHandle()).size());
+ Process.myUserHandle(), false).size());
}
/**
@@ -957,23 +1047,24 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
public void testGetByCapability() throws Exception {
mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
Mockito.mock(IConnectionService.class));
- registerAndEnableAccount(makeQuickAccountBuilder("id1", 1)
+ registerAndEnableAccount(makeQuickAccountBuilder("id1", 1, null)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
| PhoneAccount.CAPABILITY_VIDEO_CALLING)
.setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_SIP))
.build());
- registerAndEnableAccount(makeQuickAccountBuilder("id2", 2)
+ registerAndEnableAccount(makeQuickAccountBuilder("id2", 2, null)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_SIP))
.build());
assertEquals(1, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_SIP,
- false /* includeDisabled */, Process.myUserHandle()).size(),
+ false /* includeDisabled */, Process.myUserHandle(), false).size(),
PhoneAccount.CAPABILITY_VIDEO_CALLING);
assertEquals(2, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_SIP,
- false /* includeDisabled */, Process.myUserHandle()).size(), 0 /* none extra */);
+ false /* includeDisabled */, Process.myUserHandle(), false)
+ .size(), 0 /* none extra */);
assertEquals(0, mRegistrar.getCallCapablePhoneAccounts(PhoneAccount.SCHEME_SIP,
- false /* includeDisabled */, Process.myUserHandle()).size(),
+ false /* includeDisabled */, Process.myUserHandle(), false).size(),
PhoneAccount.CAPABILITY_RTT);
}
@@ -1059,8 +1150,8 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
registerAndEnableAccount(pa1);
registerAndEnableAccount(pa2);
- assertEquals(1, mRegistrar.getAllPhoneAccounts(users.get(0)).size());
- assertEquals(1, mRegistrar.getAllPhoneAccounts(users.get(1)).size());
+ assertEquals(1, mRegistrar.getAllPhoneAccounts(users.get(0), false).size());
+ assertEquals(1, mRegistrar.getAllPhoneAccounts(users.get(1), false).size());
// WHEN
@@ -1254,6 +1345,366 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
defaultPhoneAccountHandle.phoneAccountHandle.getId());
}
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccountHandle} with a { PhoneAccountHandle#packageName} that is over the
+ * character limit set
+ */
+ @Test
+ public void testInvalidPhoneAccountHandlePackageNameThrowsException() {
+ // GIVEN
+ String invalidPackageName = INVALID_STR;
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName(invalidPackageName, this.getClass().getName()), TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidPackageName,
+ account.getAccountHandle().getComponentName().getPackageName());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccountHandle} with a { PhoneAccountHandle#className} that is over the
+ * character limit set
+ */
+ @Test
+ public void testInvalidPhoneAccountHandleClassNameThrowsException() {
+ // GIVEN
+ String invalidClassName = INVALID_STR;
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName(this.getClass().getPackageName(), invalidClassName), TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidClassName,
+ account.getAccountHandle().getComponentName().getClassName());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccountHandle} with a { PhoneAccount#mId} that is over the character limit set
+ */
+ @Test
+ public void testInvalidPhoneAccountHandleIdThrowsException() {
+ // GIVEN
+ String invalidId = INVALID_STR;
+ PhoneAccountHandle handle = makeQuickAccountHandle(invalidId);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidId, account.getAccountHandle().getId());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccount} with a { PhoneAccount#mLabel} that is over the character limit set
+ */
+ @Test
+ public void testInvalidLabelThrowsException() {
+ // GIVEN
+ String invalidLabel = INVALID_STR;
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, invalidLabel)
+ .setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
+
+ // WHEN
+ when(mAppLabelProxy.getAppLabel(anyString())).thenReturn(invalidLabel);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidLabel, account.getLabel());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccount} with a {PhoneAccount#mShortDescription} that is over the character
+ * limit set
+ */
+ @Test
+ public void testInvalidShortDescriptionThrowsException() {
+ // GIVEN
+ String invalidShortDescription = INVALID_STR;
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle)
+ .setShortDescription(invalidShortDescription);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidShortDescription, account.getShortDescription());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccount} with a {PhoneAccount#mGroupId} that is over the character limit set
+ */
+ @Test
+ public void testInvalidGroupIdThrowsException() {
+ // GIVEN
+ String invalidGroupId = INVALID_STR;
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle)
+ .setGroupId(invalidGroupId);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidGroupId, account.getGroupId());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccount} with a {PhoneAccount#mExtras} that is over the character limit set
+ */
+ @Test
+ public void testInvalidExtraStringKeyThrowsException() {
+ // GIVEN
+ String invalidBundleKey = INVALID_STR;
+ String keyValue = "value";
+ Bundle extras = new Bundle();
+ extras.putString(invalidBundleKey, keyValue);
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle)
+ .setExtras(extras);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(keyValue, account.getExtras().getString(invalidBundleKey));
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccount} with a {PhoneAccount#mExtras} that is over the character limit set
+ */
+ @Test
+ public void testInvalidExtraStringValueThrowsException() {
+ // GIVEN
+ String extrasKey = "ExtrasStringKey";
+ String invalidBundleValue = INVALID_STR;
+ Bundle extras = new Bundle();
+ extras.putString(extrasKey, invalidBundleValue);
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle)
+ .setExtras(extras);
+
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidBundleValue, account.getExtras().getString(extrasKey));
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Test that an {@link IllegalArgumentException} is thrown when a package registers a
+ * {@link PhoneAccount} with a {PhoneAccount#mExtras} that is over the (key,value) pair limit
+ */
+ @Test
+ public void testInvalidExtraElementsExceedsLimitAndThrowsException() {
+ // GIVEN
+ int invalidBundleExtrasLimit =
+ PhoneAccountRegistrar.MAX_PHONE_ACCOUNT_EXTRAS_KEY_PAIR_LIMIT + 1;
+ Bundle extras = new Bundle();
+ for (int i = 0; i < invalidBundleExtrasLimit; i++) {
+ extras.putString(UUID.randomUUID().toString(), "value");
+ }
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle)
+ .setExtras(extras);
+ // THEN
+ try {
+ PhoneAccount account = builder.build();
+ assertEquals(invalidBundleExtrasLimit, account.getExtras().size());
+ mRegistrar.registerPhoneAccount(account);
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Test Pass
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when adding more than 10 schemes for a single
+ * account
+ */
+ @Test
+ public void testLimitOnSchemeCount() {
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, TEST_LABEL);
+ for (int i = 0; i < PhoneAccountRegistrar.MAX_PHONE_ACCOUNT_REGISTRATIONS + 1; i++) {
+ builder.addSupportedUriScheme(Integer.toString(i));
+ }
+ try {
+ mRegistrar.enforceLimitsOnSchemes(builder.build());
+ fail("should have hit exception in enforceLimitOnSchemes");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when adding more 256 chars for a single
+ * account
+ */
+ @Test
+ public void testLimitOnSchemeLength() {
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, TEST_LABEL);
+ builder.addSupportedUriScheme(INVALID_STR);
+ try {
+ mRegistrar.enforceLimitsOnSchemes(builder.build());
+ fail("should have hit exception in enforceLimitOnSchemes");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when adding an address over the limit
+ */
+ @Test
+ public void testLimitOnAddress() {
+ String text = "a".repeat(100);
+ PhoneAccountHandle handle = makeQuickAccountHandle(TEST_ID);
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(handle)
+ .setAddress(Uri.fromParts(text, text, text));
+ try {
+ mRegistrar.registerPhoneAccount(builder.build());
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when an Icon that throws an IOException is given
+ */
+ @Test
+ public void testLimitOnIcon() throws Exception {
+ Icon mockIcon = mock(Icon.class);
+ // GIVEN
+ PhoneAccount.Builder builder = makeBuilderWithBindCapabilities(
+ makeQuickAccountHandle(TEST_ID)).setIcon(mockIcon);
+ try {
+ // WHEN
+ Mockito.doThrow(new IOException())
+ .when(mockIcon).writeToStream(any(OutputStream.class));
+ //THEN
+ mRegistrar.enforceIconSizeLimit(builder.build());
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ assertTrue(e.getMessage().contains(PhoneAccountRegistrar.ICON_ERROR_MSG));
+ }
+ }
+
+ /**
+ * Ensure an IllegalArgumentException is thrown when providing a SubscriptionAddress that
+ * exceeds the PhoneAccountRegistrar limit.
+ */
+ @Test
+ public void testLimitOnSubscriptionAddress() throws Exception {
+ String text = "a".repeat(100);
+ PhoneAccount.Builder builder = new PhoneAccount.Builder(makeQuickAccountHandle(TEST_ID),
+ TEST_LABEL).setSubscriptionAddress(Uri.fromParts(text, text, text));
+ try {
+ mRegistrar.enforceCharacterLimit(builder.build());
+ fail("failed to throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass test
+ }
+ }
+
+ /**
+ * PhoneAccounts with CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS do not require a
+ * ConnectionService. Ensure that such an account can be registered and fetched.
+ */
+ @Test
+ public void testFetchingTransactionalAccounts() {
+ PhoneAccount account = makeBuilderWithBindCapabilities(
+ makeQuickAccountHandle(TEST_ID)).build();
+
+ try {
+ assertEquals(0, mRegistrar.getAllPhoneAccounts(null, true).size());
+ registerAndEnableAccount(account);
+ assertEquals(1, mRegistrar.getAllPhoneAccounts(null, true).size());
+ } finally {
+ mRegistrar.unregisterPhoneAccount(account.getAccountHandle());
+ }
+ }
+
+ private static PhoneAccount.Builder makeBuilderWithBindCapabilities(PhoneAccountHandle handle) {
+ return new PhoneAccount.Builder(handle, TEST_LABEL)
+ .setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
+ }
+
private static ComponentName makeQuickConnectionServiceComponentName() {
return new ComponentName(
"com.android.server.telecom.tests",
@@ -1268,9 +1719,17 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
return new PhoneAccountHandle(name, id, Process.myUserHandle());
}
- private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
+ private static PhoneAccountHandle makeQuickAccountHandleForUser(
+ String id, UserHandle userHandle) {
+ return new PhoneAccountHandle(makeQuickConnectionServiceComponentName(), id, userHandle);
+ }
+
+ private PhoneAccount.Builder makeQuickAccountBuilder(
+ String id, int idx, UserHandle userHandle) {
return new PhoneAccount.Builder(
- makeQuickAccountHandle(id),
+ userHandle == null
+ ? makeQuickAccountHandle(id)
+ : makeQuickAccountHandleForUser(id, userHandle),
"label" + idx);
}
@@ -1292,7 +1751,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
}
private PhoneAccount makeQuickAccount(String id, int idx) {
- return makeQuickAccountBuilder(id, idx)
+ return makeQuickAccountBuilder(id, idx, null)
.setAddress(Uri.parse("http://foo.com/" + idx))
.setSubscriptionAddress(Uri.parse("tel:555-000" + idx))
.setCapabilities(idx)
@@ -1309,7 +1768,7 @@ public class PhoneAccountRegistrarTest extends TelecomTestCase {
*/
private PhoneAccount makeQuickSimAccount(int simId) {
PhoneAccount simAccount =
- makeQuickAccountBuilder("sim" + simId, simId)
+ makeQuickAccountBuilder("sim" + simId, simId, null)
.setCapabilities(
PhoneAccount.CAPABILITY_CALL_PROVIDER
| PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 12420a882..a4adf776d 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -27,14 +27,17 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.app.NotificationManager;
+import android.content.ComponentName;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
@@ -42,10 +45,12 @@ import android.media.Ringtone;
import android.media.VolumeShaper;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Parcel;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.test.suitebuilder.annotation.SmallTest;
@@ -66,69 +71,39 @@ import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Spy;
-import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@RunWith(JUnit4.class)
public class RingerTest extends TelecomTestCase {
private static final Uri FAKE_RINGTONE_URI = Uri.parse("content://media/fake/audio/1729");
- private static class UriVibrationEffect extends VibrationEffect {
- final Uri mUri;
-
- private UriVibrationEffect(Uri uri) {
- mUri = uri;
- }
-
- @Override
- public VibrationEffect resolve(int defaultAmplitude) {
- return this;
- }
-
- @Override
- public VibrationEffect scale(float scaleFactor) {
- return this;
- }
-
- @Override
- public void validate() {
- // not needed
- }
-
- @Override
- public long getDuration() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- // not needed
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- UriVibrationEffect that = (UriVibrationEffect) o;
- return Objects.equals(mUri, that.mUri);
- }
- }
+ // 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);
@Mock InCallTonePlayer.Factory mockPlayerFactory;
@Mock SystemSettingsUtil mockSystemSettingsUtil;
- @Mock AsyncRingtonePlayer mockRingtonePlayer;
@Mock RingtoneFactory mockRingtoneFactory;
@Mock Vibrator mockVibrator;
@Mock InCallController mockInCallController;
@Mock NotificationManager mockNotificationManager;
+ @Mock Ringer.AccessibilityManagerAdapter mockAccessibilityManagerAdapter;
+
@Spy Ringer.VibrationEffectProxy spyVibrationEffectProxy;
@Mock InCallTonePlayer mockTonePlayer;
@Mock Call mockCall1;
@Mock Call mockCall2;
+ private static final PhoneAccountHandle PA_HANDLE =
+ new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"),
+ "pa_id");
+
+ boolean mIsHapticPlaybackSupported = true; // Note: initializeRinger() after changes.
+ AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
Ringer mRingerUnderTest;
AudioManager mockAudioManager;
- CompletableFuture<Boolean> mFuture = new CompletableFuture<>();
CompletableFuture<Void> mRingCompletionFuture = new CompletableFuture<>();
@Override
@@ -136,28 +111,33 @@ public class RingerTest extends TelecomTestCase {
public void setUp() throws Exception {
super.setUp();
mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
- doAnswer(invocation -> {
- Uri ringtoneUriForEffect = invocation.getArgument(0);
- return new UriVibrationEffect(ringtoneUriForEffect);
- }).when(spyVibrationEffectProxy).get(any(), any());
+ doReturn(URI_VIBRATION_EFFECT).when(spyVibrationEffectProxy).get(any(), any());
when(mockPlayerFactory.createPlayer(anyInt())).thenReturn(mockTonePlayer);
- mockAudioManager =
- (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mockAudioManager = mContext.getSystemService(AudioManager.class);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
- when(mockSystemSettingsUtil.isHapticPlaybackSupported(any(Context.class))).thenReturn(true);
- mockNotificationManager =
- (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ 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(mockRingtonePlayer.play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean()))
- .thenReturn(mFuture);
- mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil,
- mockRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy,
- mockInCallController);
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());
+
+ createRingerUnderTest();
+ }
+
+ /**
+ * (Re-)Creates the Ringer for the test. This needs to be called if changing final properties,
+ * like mIsHapticPlaybackSupported.
+ */
+ private void createRingerUnderTest() {
+ mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil,
+ asyncRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy,
+ mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter);
+ // This future is used to wait for AsyncRingtonePlayer to finish its part.
mRingerUnderTest.setBlockOnRingingFuture(mRingCompletionFuture);
}
@@ -169,33 +149,30 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
- public void testNoActionInTheaterMode() {
+ public void testNoActionInTheaterMode() throws Exception {
// Start call waiting to make sure that it doesn't stop when we start ringing
- mFuture.complete(false); // not using audio coupled haptics
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockSystemSettingsUtil.isTheaterModeOn(any(Context.class))).thenReturn(true);
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockTonePlayer, never()).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@SmallTest
@Test
- public void testNoActionWithExternalRinger() {
- mFuture.complete(false); // not using audio coupled haptics
+ public void testNoActionWithExternalRinger() throws Exception {
Bundle externalRingerExtra = new Bundle();
externalRingerExtra.putBoolean(TelecomManager.EXTRA_CALL_HAS_IN_BAND_RINGTONE, true);
when(mockCall1.getIntentExtras()).thenReturn(externalRingerExtra);
when(mockCall2.getIntentExtras()).thenReturn(externalRingerExtra);
// Start call waiting to make sure that it doesn't stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockTonePlayer, never()).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@@ -203,15 +180,15 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
public void testNoActionWhenDialerRings() throws Exception {
- mFuture.complete(false); // not using audio coupled haptics
// Start call waiting to make sure that it doesn't stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockInCallController.doesConnectedDialerSupportRinging()).thenReturn(true);
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ when(mockInCallController.doesConnectedDialerSupportRinging(
+ any(UserHandle.class))).thenReturn(true);
+ ensureRingerIsNotAudible();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockTonePlayer, never()).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(AudioAttributes.class));
}
@@ -219,49 +196,46 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
public void testAudioFocusStillAcquiredWhenDialerRings() throws Exception {
- mFuture.complete(false); // not using audio coupled haptics
+
// Start call waiting to make sure that it doesn't stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockInCallController.doesConnectedDialerSupportRinging()).thenReturn(true);
+ when(mockInCallController.doesConnectedDialerSupportRinging(
+ any(UserHandle.class))).thenReturn(true);
ensureRingerIsAudible();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockTonePlayer, never()).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@SmallTest
@Test
- public void testNoActionWhenCallIsSelfManaged() {
- mFuture.complete(false); // not using audio coupled haptics
+ public void testNoActionWhenCallIsSelfManaged() throws Exception {
// Start call waiting to make sure that it doesn't stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
when(mockCall2.isSelfManaged()).thenReturn(true);
// We do want to acquire audio focus when self-managed
- assertTrue(mRingerUnderTest.startRinging(mockCall2, true));
+ assertTrue(startRingingAndWaitForAsync(mockCall2, true));
+
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockTonePlayer, never()).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@SmallTest
@Test
- public void testCallWaitingButNoRingForSpecificContacts() {
- mFuture.complete(false); // not using audio coupled haptics
+ public void testCallWaitingButNoRingForSpecificContacts() throws Exception {
when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
// Start call waiting to make sure that it does stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
verify(mockTonePlayer).startTone();
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@@ -269,16 +243,19 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
public void testNoVibrateDueToAudioCoupledHaptics() throws Exception {
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
mRingerUnderTest.startCallWaiting(mockCall1);
ensureRingerIsAudible();
enableVibrationWhenRinging();
// Pretend we're using audio coupled haptics.
- mFuture.complete(true);
- assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ setIsUsingHaptics(mockRingtone, true);
+ assertTrue(startRingingAndWaitForAsync(mockCall1, false));
+ verify(mockRingtoneFactory, times(1))
+ .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), isNull(),
- eq(true) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
+ verify(mockRingtone).play();
verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
any(VibrationAttributes.class));
}
@@ -286,17 +263,24 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
public void testVibrateButNoRingForNullRingtone() throws Exception {
+ when(mockRingtoneFactory.getRingtone(
+ any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
+ .thenReturn(null);
+
mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(null);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
- mFuture.complete(false); // not using audio coupled haptics
enableVibrationWhenRinging();
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ // The ringtone isn't known to be null until the async portion after the call completes,
+ // so startRinging still returns true here as there should nominally be a ringtone.
+ // Notably, vibration still happens in this scenario.
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
- // Try to play a silent haptics ringtone
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), isNull(),
- eq(false) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
+
+ // Just the one call to mockRingtoneFactory, which returned null.
+ verify(mockRingtoneFactory).getRingtone(
+ any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
+
// Play default vibration when future completes with no audio coupled haptics
verify(mockVibrator).vibrate(eq(mRingerUnderTest.mDefaultVibrationEffect),
any(VibrationAttributes.class));
@@ -305,19 +289,21 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
public void testVibrateButNoRingForSilentRingtone() throws Exception {
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
mRingerUnderTest.startCallWaiting(mockCall1);
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
+ when(mockRingtoneFactory.getRingtone(any(Call.class), eq(null), anyBoolean()))
+ .thenReturn(mockRingtone);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
- mFuture.complete(false); // not using audio coupled haptics
enableVibrationWhenRinging();
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
// Try to play a silent haptics ringtone
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), isNull(),
- eq(false) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
+ 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),
any(VibrationAttributes.class));
@@ -325,81 +311,76 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
- public void testAudioCoupledHapticsForSilentRingtone() throws Exception {
+ public void testVibrateButNoRingForSilentRingtoneWithoutAudioHapticSupport() throws Exception {
+ mIsHapticPlaybackSupported = false;
+ createRingerUnderTest(); // Needed after changing haptic playback support.
mRingerUnderTest.startCallWaiting(mockCall1);
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
- mFuture.complete(true); // using audio coupled haptics
enableVibrationWhenRinging();
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
- // Try to play a silent haptics ringtone
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), isNull(),
- eq(false) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
- // Skip vibration for audio coupled haptics
- verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
+ verifyZeroInteractions(mockRingtoneFactory);
+
+ // Play default vibration when future completes with no audio coupled haptics
+ verify(mockVibrator).vibrate(eq(mRingerUnderTest.mDefaultVibrationEffect),
any(VibrationAttributes.class));
}
@SmallTest
@Test
- public void testStopRingingBeforeHapticsLookupComplete() throws Exception {
+ 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();
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(nullable(Call.class))).thenReturn(mockRingtone);
- when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
- mRingerUnderTest.startRinging(mockCall1, false);
- // Make sure we haven't started the vibrator yet, but have started ringing.
- verify(mockRingtonePlayer).play(nullable(RingtoneFactory.class), nullable(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
- verify(mockVibrator, never()).vibrate(nullable(VibrationEffect.class),
- nullable(VibrationAttributes.class));
- // Simulate something stopping the ringer
- mRingerUnderTest.stopRinging();
- verify(mockRingtonePlayer).stop();
- verify(mockVibrator, never()).cancel();
- // Simulate the haptics computation finishing
- mFuture.complete(false);
- // Then make sure that we don't actually start vibrating.
- verify(mockVibrator, never()).vibrate(nullable(VibrationEffect.class),
- nullable(VibrationAttributes.class));
+ 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 = mock(Ringtone.class);
+ Ringtone mockRingtone = ensureRingtoneMocked();
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
when(mockRingtone.getUri()).thenReturn(FAKE_RINGTONE_URI);
- mFuture.complete(false); // not using audio coupled haptics
enableVibrationWhenRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), eq(null),
- eq(true) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
- verify(mockVibrator).vibrate(eq(spyVibrationEffectProxy.get(FAKE_RINGTONE_URI, mContext)),
- any(VibrationAttributes.class));
+ verify(mockRingtoneFactory, times(1))
+ .getRingtone(any(Call.class), isNull(), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
+ verify(mockRingtone).play();
+ verify(spyVibrationEffectProxy).get(eq(FAKE_RINGTONE_URI), any(Context.class));
+ verify(mockVibrator).vibrate(eq(URI_VIBRATION_EFFECT), any(VibrationAttributes.class));
}
@SmallTest
@Test
public void testRingAndNoVibrate() throws Exception {
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
mRingerUnderTest.startCallWaiting(mockCall1);
ensureRingerIsAudible();
- mFuture.complete(false); // not using audio coupled haptics
enableVibrationOnlyWhenNotRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockRingtoneFactory, times(1))
+ .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), eq(null),
- eq(true) /* isRingerAudible */, eq(false) /* isVibrationEnabled */);
+ verify(mockRingtone).play();
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@@ -407,81 +388,212 @@ public class RingerTest extends TelecomTestCase {
@SmallTest
@Test
public void testRingWithRampingRinger() throws Exception {
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
mRingerUnderTest.startCallWaiting(mockCall1);
ensureRingerIsAudible();
enableRampingRinger();
- mFuture.complete(false); // not using audio coupled haptics
enableVibrationWhenRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockRingtoneFactory, times(1))
+ .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer).play(
- any(RingtoneFactory.class), any(Call.class), any(VolumeShaper.Configuration.class),
- eq(true) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
+ verify(mockRingtone).play();
}
@SmallTest
@Test
- public void testSilentRingWithHfpStillAcquiresFocus1() throws Exception {
+ public void testSilentRingWithHfpStillAcquiresFocus() throws Exception {
mRingerUnderTest.startCallWaiting(mockCall1);
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
- mFuture.complete(false); // not using audio coupled haptics
enableVibrationOnlyWhenNotRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, true));
- mRingCompletionFuture.get();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, true));
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
+ // Ringer not audible, so never tries to create a ringtone.
+ verifyZeroInteractions(mockRingtoneFactory);
verify(mockVibrator, never())
.vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
}
@SmallTest
@Test
- public void testSilentRingWithHfpStillAcquiresFocus2() throws Exception {
+ public void testRingAndVibrateForAllowedCallInDndMode() throws Exception {
mRingerUnderTest.startCallWaiting(mockCall1);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(null);
- when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
- when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
- mFuture.complete(false); // not using audio coupled haptics
- enableVibrationOnlyWhenNotRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, true));
- mRingCompletionFuture.get();
+ Ringtone mockRingtone = ensureRingtoneMocked();
+ when(mockNotificationManager.getZenMode()).thenReturn(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+ when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_SILENT);
+ when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(100);
+ enableVibrationWhenRinging();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, true));
+ verify(mockRingtoneFactory, times(1))
+ .getRingtone(any(Call.class), isNull(), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer, never()).play(any(RingtoneFactory.class), any(Call.class),
- nullable(VolumeShaper.Configuration.class), anyBoolean(), anyBoolean());
- verify(mockVibrator, never())
- .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+ verify(mockRingtone).play();
}
- @SmallTest
+ /**
+ * test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
+ * false (meaning DND is ON and the caller cannot bypass the settings)
+ */
@Test
- public void testRingAndVibrateForAllowedCallInDndMode() throws Exception {
+ public void testShouldRingForContact_CallSuppressed() {
+ // WHEN
+ when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
+ when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
+ when(mContext.getSystemService(NotificationManager.class)).thenReturn(
+ mockNotificationManager);
+ // suppress the call
+ when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+
+ // run the method under test
+ assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
+
+ // THEN
+ // verify we never set the call object and matchesCallFilter is called
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
+ verify(mockNotificationManager, times(1))
+ .matchesCallFilter(any(Bundle.class));
+ }
+
+ /**
+ * test shouldRingForContact will alert the user of an incoming call if matchesCallFilter
+ * returns true (meaning DND is NOT suppressing the caller)
+ */
+ @Test
+ public void testShouldRingForContact_CallShouldRing() {
+ // WHEN
+ 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));
+
+ // THEN
+ // verify we never set the call object and matchesCallFilter is called
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+ verify(mockNotificationManager, times(1))
+ .matchesCallFilter(any(Bundle.class));
+ }
+
+ /**
+ * ensure Telecom does not re-query the NotificationManager if the call object already has
+ * the result.
+ */
+ @Test
+ public void testShouldRingForContact_matchesCallFilterIsAlreadyComputed() {
+ // WHEN
+ when(mockCall1.wasDndCheckComputedForCall()).thenReturn(true);
+ when(mockCall1.isCallSuppressedByDoNotDisturb()).thenReturn(true);
+
+ // THEN
+ assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
+ verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+ verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
+ }
+
+ @Test
+ public void testNoFlashNotificationWhenCallSuppressed() throws Exception {
+ ensureRingtoneMocked();
+ // Start call waiting to make sure that it doesn't stop when we start ringing
mRingerUnderTest.startCallWaiting(mockCall1);
+ when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
+ when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
+ when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+
+ assertFalse(mRingerUnderTest.shouldRingForContact(mockCall2));
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockAccessibilityManagerAdapter, never())
+ .startFlashNotificationSequence(any(Context.class), anyInt());
+ }
+
+ @Test
+ public void testStartFlashNotificationWhenRingStarts() throws Exception {
+ ensureRingtoneMocked();
+ // Start call waiting to make sure that it doesn't stop when we start ringing
+ 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));
+ verify(mockAccessibilityManagerAdapter, atLeastOnce())
+ .startFlashNotificationSequence(any(Context.class), anyInt());
+ }
+
+ @Test
+ public void testStopFlashNotificationWhenRingStops() throws Exception {
Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
- when(mockNotificationManager.getZenMode()).thenReturn(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
- when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_SILENT);
- when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(100);
- mFuture.complete(true); // using audio coupled haptics
+ when(mockRingtoneFactory.getRingtone(
+ any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
+ .thenAnswer(x -> {
+ // Be slow to create ringtone.
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return mockRingtone;
+ });
+ // Start call waiting to make sure that it doesn't stop when we start ringing
enableVibrationWhenRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, true));
+ 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));
+ mRingerUnderTest.stopRinging();
+ verify(mockAccessibilityManagerAdapter, atLeastOnce())
+ .stopFlashNotificationSequence(any(Context.class));
+ mRingCompletionFuture.get(); // Don't leak async work.
+ verify(mockVibrator, never()) // cancelled before it started.
+ .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+ }
+
+ @SmallTest
+ @Test
+ public void testNoRingingForQuietProfile() throws Exception {
+ UserManager um = mContext.getSystemService(UserManager.class);
+ when(um.isManagedProfile(PA_HANDLE.getUserHandle().getIdentifier())).thenReturn(true);
+ when(um.isQuietModeEnabled(PA_HANDLE.getUserHandle())).thenReturn(true);
+ // We don't want to acquire audio focus when self-managed
+ assertFalse(startRingingAndWaitForAsync(mockCall2, true));
+
+ verify(mockTonePlayer, never()).stopTone();
+ verifyZeroInteractions(mockRingtoneFactory);
+ 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
+ * waiting for anything open-ended.
+ */
+ private boolean startRingingAndWaitForAsync(Call mockCall2, boolean isHfpDeviceAttached)
+ throws Exception {
+ boolean result = mRingerUnderTest.startRinging(mockCall2, isHfpDeviceAttached);
mRingCompletionFuture.get();
- verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), eq(null),
- eq(true) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
+ return result;
}
private void ensureRingerIsAudible() {
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(100);
}
+ private void ensureRingerIsNotAudible() {
+ when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+ when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+ }
+
private void enableVibrationWhenRinging() {
when(mockVibrator.hasVibrator()).thenReturn(true);
when(mockSystemSettingsUtil.isRingVibrationEnabled(any(Context.class))).thenReturn(true);
@@ -495,4 +607,21 @@ public class RingerTest extends TelecomTestCase {
private void enableRampingRinger() {
when(mockSystemSettingsUtil.isRampingRingerEnabled(any(Context.class))).thenReturn(true);
}
+
+ private void setIsUsingHaptics(Ringtone mockRingtone, boolean useHaptics) {
+ // Note: using haptics can also depend on mIsHapticPlaybackSupported. If changing
+ // that, the ringerUnderTest needs to be re-created.
+ when(mockSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled())
+ .thenReturn(useHaptics);
+ when(mockRingtone.hasHapticChannels()).thenReturn(useHaptics);
+ }
+
+ private Ringtone ensureRingtoneMocked() {
+ Ringtone mockRingtone = mock(Ringtone.class);
+ when(mockRingtoneFactory.getRingtone(
+ any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
+ .thenReturn(mockRingtone);
+ when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(mockRingtone);
+ return mockRingtone;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index f6f2ae283..8bc1f2a4d 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -18,11 +18,13 @@ package com.android.server.telecom.tests;
import static android.Manifest.permission.CALL_PHONE;
import static android.Manifest.permission.CALL_PRIVILEGED;
+import static android.Manifest.permission.MANAGE_OWN_CALLS;
import static android.Manifest.permission.MODIFY_PHONE_STATE;
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 android.Manifest;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.ComponentName;
@@ -31,13 +33,16 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.telecom.CallAttributes;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -45,7 +50,9 @@ import android.telecom.VideoProfile;
import android.telephony.TelephonyManager;
import android.test.suitebuilder.annotation.SmallTest;
+import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
+import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallIntentProcessor;
import com.android.server.telecom.CallState;
@@ -56,6 +63,9 @@ 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 org.junit.After;
import org.junit.Before;
@@ -66,7 +76,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
@@ -78,6 +87,7 @@ 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;
@@ -94,13 +104,18 @@ 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 {
+ private static final String CALLING_PACKAGE = TelecomServiceImplTest.class.getPackageName();
+ private static final String TEST_NAME = "Alan Turing";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
public static final String TEST_PACKAGE = "com.test";
public static final String PACKAGE_NAME = "test";
@@ -174,6 +189,9 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@Mock private UserCallIntentProcessor mUserCallIntentProcessor;
private PackageManager mPackageManager;
@Mock private ApplicationInfo mApplicationInfo;
+ @Mock private ICallEventCallback mICallEventCallback;
+ @Mock private TransactionManager mTransactionManager;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
@@ -203,6 +221,8 @@ public class TelecomServiceImplTest extends TelecomTestCase {
doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class),
anyString());
+ when(mContext.checkCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
doAnswer(invocation -> {
mDefaultDialerObserver = invocation.getArgument(1);
return null;
@@ -223,6 +243,8 @@ public class TelecomServiceImplTest extends TelecomTestCase {
mSubscriptionManagerAdapter,
mSettingsSecureAdapter,
mLock);
+ telecomServiceImpl.setTransactionManager(mTransactionManager);
+ telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
mTSIBinder = telecomServiceImpl.getBinder();
mComponentContextFixture.setTelecomManager(mTelecomManager);
when(mTelecomManager.getDefaultDialerPackage()).thenReturn(DEFAULT_DIALER_PACKAGE);
@@ -271,6 +293,51 @@ public class TelecomServiceImplTest extends TelecomTestCase {
assertEquals(SIP_PA_HANDLE_17, returnedHandleSip);
}
+ /**
+ * Clear the groupId from the PhoneAccount if a package does NOT have MODIFY_PHONE_STATE
+ */
+ @SmallTest
+ @Test
+ public void testGroupIdIsClearedWhenPermissionIsMissing() throws RemoteException {
+ // GIVEN
+ PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_CURRENT)
+ .setGroupId("testId")
+ .build();
+ // WHEN
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class), anyBoolean());
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingPermission(MODIFY_PHONE_STATE);
+ // THEN
+ PhoneAccount account =
+ mTSIBinder.getPhoneAccount(TEL_PA_HANDLE_CURRENT, PACKAGE_NAME);
+ assertEquals("***", account.getGroupId());
+ }
+
+ /**
+ * Ensure groupId is not cleared if a package has MODIFY_PHONE_STATE
+ */
+ @SmallTest
+ @Test
+ public void testGroupIdIsNotCleared() throws RemoteException {
+ // GIVEN
+ final String groupId = "testId";
+ PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_CURRENT)
+ .setGroupId(groupId)
+ .build();
+ // WHEN
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class), anyBoolean());
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingPermission(MODIFY_PHONE_STATE);
+ // THEN
+ PhoneAccount account =
+ mTSIBinder.getPhoneAccount(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE);
+ assertEquals(groupId, account.getGroupId());
+ }
+
@SmallTest
@Test
public void testGetDefaultOutgoingPhoneAccountSucceedsIfCallerIsSimCallManager()
@@ -354,19 +421,90 @@ public class TelecomServiceImplTest extends TelecomTestCase {
.setUserSelectedOutgoingPhoneAccount(eq(TEL_PA_HANDLE_16), any(UserHandle.class));
}
+ @Test
+ public void testAddCallWithOutgoingCall() throws RemoteException {
+ // GIVEN
+ CallAttributes mOutgoingCallAttributes = new CallAttributes.Builder(TEL_PA_HANDLE_CURRENT,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .build();
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(TEL_PA_HANDLE_CURRENT).build();
+ phoneAccount.setIsEnabled(true);
+
+ // WHEN
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ phoneAccount);
+
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+
+ mTSIBinder.addCall(mOutgoingCallAttributes, mICallEventCallback, "1", CALLING_PACKAGE);
+
+ // THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(OutgoingCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testAddCallWithIncomingCall() throws RemoteException {
+ // GIVEN
+ CallAttributes mIncomingCallAttributes = new CallAttributes.Builder(TEL_PA_HANDLE_CURRENT,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .build();
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(TEL_PA_HANDLE_CURRENT).build();
+ phoneAccount.setIsEnabled(true);
+
+ // WHEN
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ phoneAccount);
+
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+
+ mTSIBinder.addCall(mIncomingCallAttributes, mICallEventCallback, "1", CALLING_PACKAGE);
+
+ // THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(IncomingCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testAddCallWithManagedPhoneAccount() throws RemoteException {
+ // GIVEN
+ CallAttributes attributes = new CallAttributes.Builder(TEL_PA_HANDLE_CURRENT,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(TEL_PA_HANDLE_CURRENT)
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ .build();
+ phoneAccount.setIsEnabled(true);
+
+ // WHEN
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ phoneAccount);
+
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+
+ // THEN
+ try {
+ mTSIBinder.addCall(attributes, mICallEventCallback, "1", CALLING_PACKAGE);
+ fail("should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass
+ }
+ }
+
@SmallTest
@Test
- public void testSetUserSelectedOutgoingPhoneAccountFailure() throws RemoteException {
+ public void testSetUserSelectedOutgoingPhoneAccountWithoutPermission() throws RemoteException {
doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
anyString(), nullable(String.class));
- try {
- mTSIBinder.setUserSelectedOutgoingPhoneAccount(TEL_PA_HANDLE_16);
- } catch (SecurityException e) {
- // desired result
- }
- verify(mFakePhoneAccountRegistrar, never())
- .setUserSelectedOutgoingPhoneAccount(
- any(PhoneAccountHandle.class), any(UserHandle.class));
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.setUserSelectedOutgoingPhoneAccount(TEL_PA_HANDLE_16));
}
@SmallTest
@@ -378,11 +516,11 @@ public class TelecomServiceImplTest extends TelecomTestCase {
// Returns all phone accounts when getCallCapablePhoneAccounts is called.
when(mFakePhoneAccountRegistrar
.getCallCapablePhoneAccounts(nullable(String.class), eq(true),
- nullable(UserHandle.class))).thenReturn(fullPHList);
+ nullable(UserHandle.class), eq(true))).thenReturn(fullPHList);
// Returns only enabled phone accounts when getCallCapablePhoneAccounts is called.
when(mFakePhoneAccountRegistrar
.getCallCapablePhoneAccounts(nullable(String.class), eq(false),
- nullable(UserHandle.class))).thenReturn(smallPHList);
+ nullable(UserHandle.class), eq(true))).thenReturn(smallPHList);
makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
assertEquals(fullPHList,
@@ -395,21 +533,63 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
- public void testGetCallCapablePhoneAccountsFailure() throws RemoteException {
+ public void testGetCallCapablePhoneAccountsWithoutPermission() throws RemoteException {
List<String> enforcedPermissions = List.of(READ_PHONE_STATE, READ_PRIVILEGED_PHONE_STATE);
doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
argThat(new AnyStringIn(enforcedPermissions)), anyString());
- List<PhoneAccountHandle> result = null;
- try {
- result = mTSIBinder.getCallCapablePhoneAccounts(true, "", null).getList();
- } catch (SecurityException e) {
- // intended behavior
- }
- assertNull(result);
- verify(mFakePhoneAccountRegistrar, never())
- .getCallCapablePhoneAccounts(anyString(), anyBoolean(), any(UserHandle.class));
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null));
+ }
+
+ @SmallTest
+ @Test
+ public void testGetSelfManagedPhoneAccounts() throws RemoteException {
+ List<PhoneAccountHandle> accounts = List.of(TEL_PA_HANDLE_16);
+
+ when(mFakePhoneAccountRegistrar.getSelfManagedPhoneAccounts(nullable(UserHandle.class)))
+ .thenReturn(accounts);
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16);
+
+ assertEquals(accounts,
+ mTSIBinder.getSelfManagedPhoneAccounts(DEFAULT_DIALER_PACKAGE, null).getList());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetSelfManagedPhoneAccountsWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(READ_PHONE_STATE, READ_PRIVILEGED_PHONE_STATE);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), anyString());
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getSelfManagedPhoneAccounts("", null));
+ }
+
+ @SmallTest
+ @Test
+ public void testGetOwnSelfManagedPhoneAccounts() throws RemoteException {
+ List<PhoneAccountHandle> accounts = List.of(TEL_PA_HANDLE_16);
+
+ when(mFakePhoneAccountRegistrar.getSelfManagedPhoneAccountsForPackage(
+ eq(DEFAULT_DIALER_PACKAGE), nullable(UserHandle.class)))
+ .thenReturn(accounts);
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16);
+
+ assertEquals(accounts,
+ mTSIBinder.getOwnSelfManagedPhoneAccounts(DEFAULT_DIALER_PACKAGE, null).getList());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetOwnSelfManagedPhoneAccountsWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(MANAGE_OWN_CALLS);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), anyString());
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getOwnSelfManagedPhoneAccounts("", null));
}
@SmallTest
@@ -419,10 +599,12 @@ public class TelecomServiceImplTest extends TelecomTestCase {
List<PhoneAccountHandle> telPHList = List.of(TEL_PA_HANDLE_16);
when(mFakePhoneAccountRegistrar
- .getCallCapablePhoneAccounts(eq("tel"), anyBoolean(), any(UserHandle.class)))
+ .getCallCapablePhoneAccounts(eq("tel"), anyBoolean(),
+ any(UserHandle.class), anyBoolean()))
.thenReturn(telPHList);
when(mFakePhoneAccountRegistrar
- .getCallCapablePhoneAccounts(eq("sip"), anyBoolean(), any(UserHandle.class)))
+ .getCallCapablePhoneAccounts(eq("sip"), anyBoolean(),
+ any(UserHandle.class), anyBoolean()))
.thenReturn(sipPHList);
makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
@@ -436,11 +618,21 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testGetPhoneAccountsSupportingSchemeWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(MODIFY_PHONE_STATE);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), anyString());
+
+ assertTrue(mTSIBinder.getPhoneAccountsSupportingScheme("any", "").getList().isEmpty());
+ }
+
+ @SmallTest
+ @Test
public void testGetPhoneAccountsForPackage() throws RemoteException {
List<PhoneAccountHandle> phoneAccountHandleList = List.of(
TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
when(mFakePhoneAccountRegistrar
- .getPhoneAccountsForPackage(anyString(), any(UserHandle.class)))
+ .getAllPhoneAccountHandlesForPackage(any(UserHandle.class), anyString()))
.thenReturn(phoneAccountHandleList);
makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
assertEquals(phoneAccountHandleList,
@@ -450,7 +642,20 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testGetPhoneAccountsForPackageWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(READ_PRIVILEGED_PHONE_STATE);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), any());
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getPhoneAccountsForPackage(""));
+ }
+
+ @SmallTest
+ @Test
public void testGetPhoneAccount() throws Exception {
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingPermission(MODIFY_PHONE_STATE);
makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
assertEquals(TEL_PA_HANDLE_16, mTSIBinder.getPhoneAccount(TEL_PA_HANDLE_16,
mContext.getPackageName()).getAccountHandle());
@@ -466,15 +671,96 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testGetAllPhoneAccountsCount() throws RemoteException {
+ List<PhoneAccount> phoneAccountList = List.of(
+ makePhoneAccount(TEL_PA_HANDLE_16).build(),
+ makePhoneAccount(SIP_PA_HANDLE_17).build());
+
+ when(mFakePhoneAccountRegistrar.getAllPhoneAccounts(any(UserHandle.class), anyBoolean()))
+ .thenReturn(phoneAccountList);
+
+ assertEquals(phoneAccountList.size(), mTSIBinder.getAllPhoneAccountsCount());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetAllPhoneAccountsCountWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(MODIFY_PHONE_STATE);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), any());
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getAllPhoneAccountsCount());
+ }
+
+ @SmallTest
+ @Test
public void testGetAllPhoneAccounts() throws RemoteException {
List<PhoneAccount> phoneAccountList = List.of(
makePhoneAccount(TEL_PA_HANDLE_16).build(),
makePhoneAccount(SIP_PA_HANDLE_17).build());
- when(mFakePhoneAccountRegistrar.getAllPhoneAccounts(any(UserHandle.class)))
+ when(mFakePhoneAccountRegistrar.getAllPhoneAccounts(any(UserHandle.class), anyBoolean()))
.thenReturn(phoneAccountList);
- assertEquals(2, mTSIBinder.getAllPhoneAccounts().getList().size());
+ assertEquals(phoneAccountList.size(), mTSIBinder.getAllPhoneAccounts().getList().size());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetAllPhoneAccountsWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(MODIFY_PHONE_STATE);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), any());
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getAllPhoneAccounts());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetAllPhoneAccountHandles() throws RemoteException {
+ List<PhoneAccountHandle> handles = List.of(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+ when(mFakePhoneAccountRegistrar.getAllPhoneAccountHandles(
+ any(UserHandle.class), anyBoolean())).thenReturn(handles);
+
+ assertEquals(handles, mTSIBinder.getAllPhoneAccountHandles().getList());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetAllPhoneAccountHandlesWithoutPermission() throws RemoteException {
+ List<String> enforcedPermissions = List.of(MODIFY_PHONE_STATE);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), any());
+
+ assertThrows(SecurityException.class,
+ () -> mTSIBinder.getAllPhoneAccountHandles());
+ }
+
+ @SmallTest
+ @Test
+ public void testGetSimCallManager() throws RemoteException {
+ final PhoneAccountHandle handle = TEL_PA_HANDLE_16;
+ final int subId = 1;
+ when(mFakePhoneAccountRegistrar.getSimCallManager(eq(subId), any(UserHandle.class)))
+ .thenReturn(handle);
+
+ assertEquals(handle, mTSIBinder.getSimCallManager(subId, "any"));
+ }
+
+ @SmallTest
+ @Test
+ public void testGetSimCallManagerForUser() throws RemoteException {
+ final PhoneAccountHandle handle = TEL_PA_HANDLE_16;
+ final int user = 1;
+ when(mFakePhoneAccountRegistrar.getSimCallManager(
+ argThat(userHandle -> {
+ return userHandle.getIdentifier() == user;
+ })))
+ .thenReturn(handle);
+
+ assertEquals(handle, mTSIBinder.getSimCallManagerForUser(user, "any"));
}
@SmallTest
@@ -492,6 +778,65 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testRegisterPhoneAccountWithoutPermissionAnomalyReported() throws RemoteException {
+ PhoneAccountHandle handle = new PhoneAccountHandle(
+ new ComponentName("package", "cs"), "test", Binder.getCallingUserHandle());
+ PhoneAccount account = makeSelfManagedPhoneAccount(handle).build();
+
+ List<String> enforcedPermissions = List.of(MANAGE_OWN_CALLS);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), any());
+
+ registerPhoneAccountTestHelper(account, false);
+ verify(mAnomalyReporterAdapter).reportAnomaly(
+ TelecomServiceImpl.REGISTER_PHONE_ACCOUNT_ERROR_UUID,
+ TelecomServiceImpl.REGISTER_PHONE_ACCOUNT_ERROR_MSG);
+ }
+
+ @SmallTest
+ @Test
+ public void testRegisterPhoneAccountSelfManagedWithoutPermission() throws RemoteException {
+ PhoneAccountHandle handle = new PhoneAccountHandle(
+ new ComponentName("package", "cs"), "test", Binder.getCallingUserHandle());
+ PhoneAccount account = makeSelfManagedPhoneAccount(handle).build();
+
+ List<String> enforcedPermissions = List.of(MANAGE_OWN_CALLS);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), any());
+
+ registerPhoneAccountTestHelper(account, false);
+ }
+
+ @SmallTest
+ @Test
+ public void testRegisterPhoneAccountSelfManagedInvalidCapabilities() throws RemoteException {
+ PhoneAccountHandle handle = new PhoneAccountHandle(
+ new ComponentName("package", "cs"), "test", Binder.getCallingUserHandle());
+
+ PhoneAccount selfManagedCallProviderAccount = makePhoneAccount(handle)
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .build();
+ registerPhoneAccountTestHelper(selfManagedCallProviderAccount, false);
+
+ PhoneAccount selfManagedConnectionManagerAccount = makePhoneAccount(handle)
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
+ .build();
+ registerPhoneAccountTestHelper(selfManagedConnectionManagerAccount, false);
+
+ PhoneAccount selfManagedSimSubscriptionAccount = makePhoneAccount(handle)
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ .build();
+ registerPhoneAccountTestHelper(selfManagedSimSubscriptionAccount, false);
+ }
+
+ @SmallTest
+ @Test
public void testRegisterPhoneAccountWithoutModifyPermission() throws RemoteException {
// tests the case where the package does not have MODIFY_PHONE_STATE but is
// registering its own phone account as a third-party connection service
@@ -589,7 +934,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
boolean didExceptionOccur = false;
try {
- mTSIBinder.registerPhoneAccount(testPhoneAccount);
+ mTSIBinder.registerPhoneAccount(testPhoneAccount, CALLING_PACKAGE);
} catch (Exception e) {
didExceptionOccur = true;
}
@@ -606,6 +951,26 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testRegisterPhoneAccountImageIconCrossUser() throws RemoteException {
+ String packageNameToUse = "com.android.officialpackage";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "test", Binder.getCallingUserHandle());
+ Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+ PhoneAccount phoneAccount = makePhoneAccount(phHandle).setIcon(icon).build();
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+
+ // This should fail; security exception will be thrown.
+ registerPhoneAccountTestHelper(phoneAccount, false);
+
+ icon = Icon.createWithContentUri("content://0@media/external/images/media/");
+ phoneAccount = makePhoneAccount(phHandle).setIcon(icon).build();
+ // This should succeed.
+ registerPhoneAccountTestHelper(phoneAccount, true);
+ }
+
+ @SmallTest
+ @Test
public void testUnregisterPhoneAccount() throws RemoteException {
String packageNameToUse = "com.android.officialpackage";
PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
@@ -615,7 +980,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
doReturn(PackageManager.PERMISSION_GRANTED)
.when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
- mTSIBinder.unregisterPhoneAccount(phHandle);
+ mTSIBinder.unregisterPhoneAccount(phHandle, CALLING_PACKAGE);
verify(mFakePhoneAccountRegistrar).unregisterPhoneAccount(phHandle);
}
@@ -632,7 +997,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
when(pm.hasSystemFeature(PackageManager.FEATURE_TELECOM)).thenReturn(false);
try {
- mTSIBinder.unregisterPhoneAccount(phHandle);
+ mTSIBinder.unregisterPhoneAccount(phHandle, CALLING_PACKAGE);
} catch (UnsupportedOperationException e) {
// expected behavior
}
@@ -644,25 +1009,47 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testClearAccounts() throws RemoteException {
+ mTSIBinder.clearAccounts(CALLING_PACKAGE);
+
+ verify(mFakePhoneAccountRegistrar)
+ .clearAccounts(CALLING_PACKAGE, mTSIBinder.getCallingUserHandle());
+ }
+
+ @SmallTest
+ @Test
+ public void testClearAccountsWithoutPermission() throws RemoteException {
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+
+ assertThrows(UnsupportedOperationException.class,
+ () -> mTSIBinder.clearAccounts(CALLING_PACKAGE));
+ }
+
+ @SmallTest
+ @Test
public void testAddNewIncomingCall() throws Exception {
- PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_CURRENT).build();
+ PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_16).build();
phoneAccount.setIsEnabled(true);
doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
- eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+ eq(TEL_PA_HANDLE_16), any(UserHandle.class));
doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
Bundle extras = createSampleExtras();
- mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_CURRENT, extras);
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+ verify(mFakePhoneAccountRegistrar).getPhoneAccount(
+ TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
- CallIntentProcessor.KEY_IS_INCOMING_CALL, extras, false);
+ CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
+ TEL_PA_HANDLE_16, false);
}
@SmallTest
@Test
public void testAddNewIncomingCallFailure() throws Exception {
try {
- mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null);
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null, CALLING_PACKAGE);
} catch (SecurityException e) {
// expected
}
@@ -670,7 +1057,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
doThrow(new SecurityException()).when(mAppOpsManager).checkPackage(anyInt(), anyString());
try {
- mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_CURRENT, null);
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_CURRENT, null, CALLING_PACKAGE);
} catch (SecurityException e) {
// expected
}
@@ -693,7 +1080,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
mTSIBinder.addNewUnknownCall(TEL_PA_HANDLE_CURRENT, extras);
addCallTestHelper(TelecomManager.ACTION_NEW_UNKNOWN_CALL,
- CallIntentProcessor.KEY_IS_UNKNOWN_CALL, extras, true);
+ CallIntentProcessor.KEY_IS_UNKNOWN_CALL, extras, TEL_PA_HANDLE_CURRENT, true);
}
@SmallTest
@@ -719,7 +1106,8 @@ public class TelecomServiceImplTest extends TelecomTestCase {
}
private void addCallTestHelper(String expectedAction, String extraCallKey,
- Bundle expectedExtras, boolean isUnknown) {
+ Bundle expectedExtras, PhoneAccountHandle expectedPhoneAccountHandle,
+ boolean isUnknown) {
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
if (isUnknown) {
verify(mCallIntentProcessorAdapter).processUnknownCallIntent(any(CallsManager.class),
@@ -731,7 +1119,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
Intent capturedIntent = intentCaptor.getValue();
assertEquals(expectedAction, capturedIntent.getAction());
Bundle intentExtras = capturedIntent.getExtras();
- assertEquals(TEL_PA_HANDLE_CURRENT,
+ assertEquals(expectedPhoneAccountHandle,
intentExtras.get(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
assertTrue(intentExtras.getBoolean(extraCallKey));
@@ -747,88 +1135,517 @@ public class TelecomServiceImplTest extends TelecomTestCase {
}
}
+ /**
+ * Place a managed call with no PhoneAccount specified and ensure no security exception is
+ * thrown.
+ */
@SmallTest
@Test
public void testPlaceCallWithNonEmergencyPermission() throws Exception {
Uri handle = Uri.parse("tel:6505551234");
Bundle extras = createSampleExtras();
+ // We have passed in the DEFAULT_DIALER_PACKAGE for this test, so canCallPhone is always
+ // true.
when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
nullable(String.class), nullable(String.class)))
.thenReturn(AppOpsManager.MODE_ALLOWED);
doReturn(PackageManager.PERMISSION_GRANTED)
- .when(mContext).checkCallingPermission(CALL_PHONE);
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
doReturn(PackageManager.PERMISSION_DENIED)
- .when(mContext).checkCallingPermission(CALL_PRIVILEGED);
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE, null);
- placeCallTestHelper(handle, extras, true);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ false,
+ /*shouldNonEmergencyBeAllowed*/ true);
}
+ /**
+ * Ensure that we get a SecurityException if the UID of the caller doesn't match the UID of the
+ * UID of the package name passed in.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_enforceCallingPackageFailure() throws Exception {
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage matches the PhoneAccountHandle, so this is an app with a self-managed
+ // ConnectionService.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // Return a non-matching UID for testing purposes.
+ when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(-1);
+ try {
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME, null);
+ fail("Expected SecurityException because calling package doesn't match");
+ } catch(SecurityException e) {
+ // expected
+ }
+ }
+
+ /**
+ * In the case that there is a self-managed call request and MANAGE_OWN_CALLS is granted, ensure
+ * that placeCall does not generate a SecurityException.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_selfManaged_permissionGranted() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makeSelfManagedPhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage matches the PhoneAccountHandle, so this is an app with a self-managed
+ // ConnectionService.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // pass MANAGE_OWN_CALLS check, but do not have CALL_PHONE
+ doNothing().when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+
+ try {
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME, null);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ true,
+ /*shouldNonEmergencyBeAllowed*/ false);
+ } catch(SecurityException e) {
+ fail("Unexpected SecurityException - MANAGE_OWN_CALLS is set");
+ }
+ }
+
+ /**
+ * In the case that the placeCall API is being used place a self-managed call
+ * (phone account is marked self-managed and the calling application owns that PhoneAccount),
+ * ensure that the call gets placed as not self-managed as to not disclose PA info.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_selfManaged_noPermission() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makeSelfManagedPhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage matches the PhoneAccountHandle, so this is an app with a self-managed
+ // ConnectionService.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+
+ try {
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME, null);
+ fail("Expected SecurityException because MANAGE_OWN_CALLS is not set");
+ } catch(SecurityException e) {
+ // expected
+ }
+ }
+
+ /**
+ * In the case that there is a self-managed call request and the app doesn't own that
+ * PhoneAccount, we will need to check CALL_PHONE. If they do not have CALL_PHONE permission,
+ * we need to throw a security exception.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_selfManaged_permissionFail() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makeSelfManagedPhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage doesn't match the PhoneAccountHandle package
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // pass MANAGE_OWN_CALLS check, but do not have CALL PHONE
+ doNothing().when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+ doThrow(new SecurityException())
+ .when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+
+ try {
+ // Calling package is received and is not the same as PACKAGE_NAME
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME + "2", null);
+ fail("Expected a SecurityException - CALL_PHONE was not granted");
+ } catch(SecurityException e) {
+ // expected
+ }
+ }
+
+ /**
+ * In the case that there is a self-managed call request and the app doesn't own that
+ * PhoneAccount, we will need to check CALL_PHONE. If they have the CALL_PHONE permission, but
+ * the app op has been denied, this should throw a security exception.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_selfManaged_appOpPermissionFail() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makeSelfManagedPhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage doesn't match the PhoneAccountHandle package.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // pass MANAGE_OWN_CALLS check, but do not have CALL PHONE
+ doNothing().when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+ doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+ try {
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME + "2", null);
+ fail("Expected a SecurityException - CALL_PHONE app op is denied");
+ } catch(SecurityException e) {
+ // expected
+ }
+ }
+
+ /**
+ * In the case that there is a self-managed call request and the app doesn't own that
+ * PhoneAccount, we will need to check CALL_PHONE. If they have the correct permissions, the
+ * call will go through, however we will have removed the self-managed PhoneAccountHandle. The
+ * call will go through as a normal managed call request with no PhoneAccountHandle.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_selfManaged_differentCallingPackage() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makeSelfManagedPhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage doesn't match the PhoneAccountHandle package
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // simulate default dialer so CALL_PHONE is granted.
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+
+ // We expect the call to go through with no PhoneAccount specified, since the request
+ // contained a self-managed PhoneAccountHandle that didn't belong to this app.
+ Bundle expectedExtras = extras.deepCopy();
+ expectedExtras.remove(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ try {
+ mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE, null);
+ } catch (SecurityException e) {
+ fail("Unexpected SecurityException - CTS is default dialer and MANAGE_OWN_CALLS is not"
+ + " required. Exception: " + e);
+ }
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ false,
+ /*shouldNonEmergencyBeAllowed*/ true);
+ }
+
+ /**
+ * In the case that there is a managed call request and the app owns that
+ * PhoneAccount (but is not a self-managed), we will still need to check CALL_PHONE.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_samePackage_managedPhoneAccount_permissionFail() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makePhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage doesn't match the PhoneAccountHandle package
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // CALL_PHONE is not granted to the device.
+ doThrow(new SecurityException())
+ .when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+ doThrow(new SecurityException())
+ .when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+
+ try {
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME + "2", null);
+ fail("Expected a SecurityException - CALL_PHONE is not granted");
+ } catch(SecurityException e) {
+ // expected
+ }
+ }
+
+ /**
+ * In the case that there is a managed call request and the app owns that
+ * PhoneAccount (but is not a self-managed), we will still need to check CALL_PHONE.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_samePackage_managedPhoneAccount_AppOpFail() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makePhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage matches the PhoneAccountHandle, but this is not a self managed phone
+ // account.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // CALL_PHONE is granted, but the app op is not
+ doThrow(new SecurityException())
+ .when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+ doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+
+ try {
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME + "2", null);
+ fail("Expected a SecurityException - CALL_PHONE app op is denied");
+ } catch(SecurityException e) {
+ // expected
+ }
+ }
+
+ /**
+ * Since this is a self-managed call being requested, so ensure we report the call as
+ * self-managed and without non-emergency permissions.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_selfManaged_nonEmergencyPermission() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ makeSelfManagedPhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage matches the PhoneAccountHandle, so this is an app with a self-managed
+ // ConnectionService.
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // enforceCallingOrSelfPermission is implicitly granted for MANAGE_OWN_CALLS here and
+ // CALL_PHONE is not required.
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_IGNORED);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+
+ mTSIBinder.placeCall(handle, extras, PACKAGE_NAME, null);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ true,
+ /*shouldNonEmergencyBeAllowed*/ false);
+ }
+
+ /**
+ * Default dialer is calling placeCall and has CALL_PHONE granted, so non-emergency calls
+ * are allowed.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCall_managed_nonEmergencyGranted() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+ // callingPackage doesn't match the PhoneAccountHandle, so this app does not have a
+ // self-managed ConnectionService
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEL_PA_HANDLE_CURRENT);
+
+ // CALL_PHONE granted
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+
+ mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE, null);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ false,
+ /*shouldNonEmergencyBeAllowed*/ true);
+ }
+
+ /**
+ * In the case that there is a managed normal call request and the app has CALL_PRIVILEGED
+ * permission, place call should complete successfully.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCallPrivileged() throws Exception {
+ doReturn(false).when(mDefaultDialerCache).isDefaultOrSystemDialer(
+ eq(DEFAULT_DIALER_PACKAGE), anyInt());
+ Uri handle = Uri.parse("tel:6505551234");
+
+ // CALL_PHONE is not granted, but CALL_PRIVILEGED is
+ doThrow(new SecurityException())
+ .when(mContext).enforceCallingOrSelfPermission(
+ eq(Manifest.permission.MANAGE_OWN_CALLS), anyString());
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ eq(CALL_PHONE), anyString());
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_ERRORED);
+
+ try {
+ mTSIBinder.placeCall(handle, null, PACKAGE_NAME + "2", null);
+ } catch(SecurityException e) {
+ fail("Expected no SecurityException - CALL_PRIVILEGED is granted");
+ }
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mUserCallIntentProcessor).processIntent(intentCaptor.capture(), anyString(),
+ eq(false), eq(true), eq(true));
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(Intent.ACTION_CALL_PRIVILEGED, capturedIntent.getAction());
+ assertEquals(handle, capturedIntent.getData());
+ }
+
+ /**
+ * The default dialer is requesting to place a call and CALL_PHONE is granted, however
+ * OP_CALL_PHONE app op is denied to that app, so non-emergency calls will be denied.
+ */
@SmallTest
@Test
public void testPlaceCallWithAppOpsOff() throws Exception {
Uri handle = Uri.parse("tel:6505551234");
Bundle extras = createSampleExtras();
+ // We have passed in the DEFAULT_DIALER_PACKAGE for this test, so canCallPhone is always
+ // true.
when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
nullable(String.class), nullable(String.class)))
.thenReturn(AppOpsManager.MODE_IGNORED);
doReturn(PackageManager.PERMISSION_GRANTED)
- .when(mContext).checkCallingPermission(CALL_PHONE);
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
doReturn(PackageManager.PERMISSION_DENIED)
- .when(mContext).checkCallingPermission(CALL_PRIVILEGED);
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE, null);
- placeCallTestHelper(handle, extras, false);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ false,
+ /*shouldNonEmergencyBeAllowed*/ false);
}
+ /**
+ * The default dialer is requesting to place a call, however CALL_PHONE is denied to that app,
+ * so non-emergency calls will be denied.
+ */
@SmallTest
@Test
public void testPlaceCallWithNoCallingPermission() throws Exception {
Uri handle = Uri.parse("tel:6505551234");
Bundle extras = createSampleExtras();
+ // We are assumed to be default dialer in this test, so canCallPhone is always true.
when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
nullable(String.class), nullable(String.class)))
.thenReturn(AppOpsManager.MODE_ALLOWED);
doReturn(PackageManager.PERMISSION_DENIED)
- .when(mContext).checkCallingPermission(CALL_PHONE);
+ .when(mContext).checkCallingOrSelfPermission(CALL_PHONE);
doReturn(PackageManager.PERMISSION_DENIED)
- .when(mContext).checkCallingPermission(CALL_PRIVILEGED);
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE, null);
- placeCallTestHelper(handle, extras, false);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ false,
+ /*shouldNonEmergencyBeAllowed*/ false);
}
+ /**
+ * Ensure the expected handle, extras, and non-emergency call permission checks have been
+ * correctly included in the ACTION_CALL intent as part of the
+ * {@link UserCallIntentProcessor#processIntent} method called during the placeCall procedure.
+ * @param expectedHandle Expected outgoing number handle
+ * @param expectedExtras Expected extras in the ACTION_CALL intent.
+ * @param shouldNonEmergencyBeAllowed true if non-emergency calls should be allowed, false if
+ * permission checks failed for non-emergency.
+ */
private void placeCallTestHelper(Uri expectedHandle, Bundle expectedExtras,
- boolean shouldNonEmergencyBeAllowed) {
+ boolean isSelfManagedExpected, boolean shouldNonEmergencyBeAllowed) {
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mUserCallIntentProcessor).processIntent(intentCaptor.capture(), anyString(),
- eq(shouldNonEmergencyBeAllowed), eq(true));
+ eq(isSelfManagedExpected), eq(shouldNonEmergencyBeAllowed), eq(true));
Intent capturedIntent = intentCaptor.getValue();
assertEquals(Intent.ACTION_CALL, capturedIntent.getAction());
assertEquals(expectedHandle, capturedIntent.getData());
assertTrue(areBundlesEqual(expectedExtras, capturedIntent.getExtras()));
}
+ /**
+ * Ensure that if the caller was never granted CALL_PHONE (and is not the default dialer), a
+ * SecurityException is thrown.
+ */
@SmallTest
@Test
public void testPlaceCallFailure() throws Exception {
Uri handle = Uri.parse("tel:6505551234");
Bundle extras = createSampleExtras();
+ // The app is not considered a privileged dialer and does not have the CALL_PHONE
+ // permission.
doThrow(new SecurityException())
.when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+
+ try {
+ mTSIBinder.placeCall(handle, extras, "arbitrary_package_name", null);
+ fail("Expected SecurityException because CALL_PHONE was not granted to caller");
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ verify(mUserCallIntentProcessor, never())
+ .processIntent(any(Intent.class), anyString(), eq(false), anyBoolean(), eq(true));
+ }
+
+ /**
+ * Ensure that if the caller was granted CALL_PHONE, but did not get the OP_CALL_PHONE app op
+ * (and is not the default dialer), a SecurityException is thrown.
+ */
+ @SmallTest
+ @Test
+ public void testPlaceCallAppOpFailure() throws Exception {
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+
+ // The app is not considered a privileged dialer and does not have the OP_CALL_PHONE
+ // app op.
+ doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(CALL_PRIVILEGED);
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString(),
+ nullable(String.class), nullable(String.class)))
+ .thenReturn(AppOpsManager.MODE_IGNORED);
try {
mTSIBinder.placeCall(handle, extras, "arbitrary_package_name", null);
+ fail("Expected SecurityException because CALL_PHONE was not granted to caller");
} catch (SecurityException e) {
// expected
}
verify(mUserCallIntentProcessor, never())
- .processIntent(any(Intent.class), anyString(), anyBoolean(), eq(true));
+ .processIntent(any(Intent.class), anyString(), eq(false), anyBoolean(), eq(true));
}
@SmallTest
@@ -1090,6 +1907,29 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
+ public void testGetDefaultDialerPackageForUser() throws Exception {
+ final int userId = 1;
+ final String packageName = "some.package";
+
+ when(mDefaultDialerCache.getDefaultDialerApplication(userId))
+ .thenReturn(packageName);
+
+ assertEquals(packageName, mTSIBinder.getDefaultDialerPackageForUser(userId));
+ }
+
+ @SmallTest
+ @Test
+ public void testGetSystemDialerPackage() throws Exception {
+ final String packageName = "some.package";
+
+ when(mDefaultDialerCache.getSystemDialerApplication())
+ .thenReturn(packageName);
+
+ assertEquals(packageName, mTSIBinder.getSystemDialerPackage(CALLING_PACKAGE));
+ }
+
+ @SmallTest
+ @Test
public void testEndCallWithRingingForegroundCall() throws Exception {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.RINGING);
@@ -1123,8 +1963,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
public void testEndCallWithNoForegroundCall() throws Exception {
Call call = mock(Call.class);
when(call.getState()).thenReturn(CallState.ACTIVE);
- when(mFakeCallsManager.getFirstCallWithState(any()))
- .thenReturn(call);
+ when(mFakeCallsManager.getFirstCallWithState(any())).thenReturn(call);
assertTrue(mTSIBinder.endCall(TEST_PACKAGE));
verify(mFakeCallsManager).disconnectCall(eq(call));
}
@@ -1165,14 +2004,16 @@ public class TelecomServiceImplTest extends TelecomTestCase {
@SmallTest
@Test
public void testIsInCall() throws Exception {
- when(mFakeCallsManager.hasOngoingCalls()).thenReturn(true);
+ when(mFakeCallsManager.hasOngoingCalls(any(UserHandle.class), anyBoolean()))
+ .thenReturn(true);
assertTrue(mTSIBinder.isInCall(DEFAULT_DIALER_PACKAGE, null));
}
@SmallTest
@Test
public void testNotIsInCall() throws Exception {
- when(mFakeCallsManager.hasOngoingCalls()).thenReturn(false);
+ when(mFakeCallsManager.hasOngoingCalls(any(UserHandle.class), anyBoolean()))
+ .thenReturn(false);
assertFalse(mTSIBinder.isInCall(DEFAULT_DIALER_PACKAGE, null));
}
@@ -1187,20 +2028,22 @@ public class TelecomServiceImplTest extends TelecomTestCase {
} catch (SecurityException e) {
// desired result
}
- verify(mFakeCallsManager, never()).hasOngoingCalls();
+ verify(mFakeCallsManager, never()).hasOngoingCalls(any(UserHandle.class), anyBoolean());
}
@SmallTest
@Test
public void testIsInManagedCall() throws Exception {
- when(mFakeCallsManager.hasOngoingManagedCalls()).thenReturn(true);
+ when(mFakeCallsManager.hasOngoingManagedCalls(any(UserHandle.class), anyBoolean()))
+ .thenReturn(true);
assertTrue(mTSIBinder.isInManagedCall(DEFAULT_DIALER_PACKAGE, null));
}
@SmallTest
@Test
public void testNotIsInManagedCall() throws Exception {
- when(mFakeCallsManager.hasOngoingManagedCalls()).thenReturn(false);
+ when(mFakeCallsManager.hasOngoingManagedCalls(any(UserHandle.class), anyBoolean()))
+ .thenReturn(false);
assertFalse(mTSIBinder.isInManagedCall(DEFAULT_DIALER_PACKAGE, null));
}
@@ -1215,7 +2058,7 @@ public class TelecomServiceImplTest extends TelecomTestCase {
} catch (SecurityException e) {
// desired result
}
- verify(mFakeCallsManager, never()).hasOngoingCalls();
+ verify(mFakeCallsManager, never()).hasOngoingCalls(any(UserHandle.class), anyBoolean());
}
/**
@@ -1251,6 +2094,22 @@ public class TelecomServiceImplTest extends TelecomTestCase {
verify(mFakeCallsManager, never()).answerCall(eq(call), anyInt());
}
+ @SmallTest
+ @Test
+ public void testGetAdnUriForPhoneAccount() throws Exception {
+ final int subId = 1;
+ final Uri adnUri = Uri.parse("content://icc/adn/subId/" + subId);
+ PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_CURRENT).build();
+ when(mFakePhoneAccountRegistrar.getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class)))
+ .thenReturn(phoneAccount);
+ when(mFakePhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(TEL_PA_HANDLE_CURRENT))
+ .thenReturn(subId);
+
+ assertEquals(adnUri,
+ mTSIBinder.getAdnUriForPhoneAccount(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE));
+ }
+
/**
* Register phone accounts for the supplied PhoneAccountHandles to make them
* visible to all users (via the isVisibleToCaller method in TelecomServiceImpl.
@@ -1275,6 +2134,12 @@ public class TelecomServiceImplTest extends TelecomTestCase {
return paBuilder;
}
+ private PhoneAccount.Builder makeSelfManagedPhoneAccount(PhoneAccountHandle paHandle) {
+ PhoneAccount.Builder paBuilder = makePhoneAccount(paHandle);
+ paBuilder.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED);
+ return paBuilder;
+ }
+
private PhoneAccount.Builder makePhoneAccount(PhoneAccountHandle paHandle) {
return new PhoneAccount.Builder(paHandle, "testLabel");
}
@@ -1286,6 +2151,8 @@ public class TelecomServiceImplTest extends TelecomTestCase {
}
private static boolean areBundlesEqual(Bundle b1, Bundle b2) {
+ if (b1.keySet().size() != b2.keySet().size()) return false;
+
for (String key1 : b1.keySet()) {
if (!b1.get(key1).equals(b2.get(key1))) {
return false;
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index d6ff196b9..b962b2a2d 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -66,7 +66,6 @@ import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyRegistryManager;
-import android.text.TextUtils;
import com.android.internal.telecom.IInCallAdapter;
import com.android.server.telecom.AsyncRingtonePlayer;
@@ -89,6 +88,7 @@ import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
import com.android.server.telecom.ProximitySensorManager;
import com.android.server.telecom.ProximitySensorManagerFactory;
+import com.android.server.telecom.Ringer;
import com.android.server.telecom.RoleManagerAdapter;
import com.android.server.telecom.StatusBarNotifier;
import com.android.server.telecom.SystemStateHelper;
@@ -96,6 +96,7 @@ import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.WiredHeadsetManager;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.ui.IncomingCallNotifier;
@@ -112,6 +113,7 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
@@ -119,6 +121,7 @@ import java.util.concurrent.TimeUnit;
*/
public class TelecomSystemTest extends TelecomTestCase {
+ private static final String CALLING_PACKAGE = TelecomSystemTest.class.getPackageName();
static final int TEST_POLL_INTERVAL = 10; // milliseconds
static final int TEST_TIMEOUT = 1000; // milliseconds
@@ -208,6 +211,10 @@ public class TelecomSystemTest extends TelecomTestCase {
@Mock ToneGenerator mToneGenerator;
@Mock DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
+ @Mock Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
+ @Mock
+ BlockedNumbersAdapter mBlockedNumbersAdapter;
+
final ComponentName mInCallServiceComponentNameX =
new ComponentName(
"incall-service-package-X",
@@ -379,13 +386,16 @@ public class TelecomSystemTest extends TelecomTestCase {
@Override
public void tearDown() throws Exception {
- mTelecomSystem.getCallsManager().waitOnHandlers();
- LinkedList<HandlerThread> handlerThreads = mTelecomSystem.getCallsManager()
- .getGraphHandlerThreads();
- for (HandlerThread handlerThread : handlerThreads) {
- handlerThread.quitSafely();
+ if (mTelecomSystem != null && mTelecomSystem.getCallsManager() != null) {
+ mTelecomSystem.getCallsManager().waitOnHandlers();
+ LinkedList<HandlerThread> handlerThreads = mTelecomSystem.getCallsManager()
+ .getGraphHandlerThreads();
+ for (HandlerThread handlerThread : handlerThreads) {
+ handlerThread.quitSafely();
+ }
+ handlerThreads.clear();
+ mTelecomSystem.getCallsManager().getVoipCallMonitor().stopMonitor();
}
- handlerThreads.clear();
waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
waitForHandlerAction(mHandlerThread.getThreadHandler(), TEST_TIMEOUT);
// Bring down the threads that are active.
@@ -396,12 +406,19 @@ public class TelecomSystemTest extends TelecomTestCase {
// don't do anything
}
- mConnectionServiceFocusManager.getHandler().removeCallbacksAndMessages(null);
- waitForHandlerAction(mConnectionServiceFocusManager.getHandler(), TEST_TIMEOUT);
- mConnectionServiceFocusManager.getHandler().getLooper().quit();
+ if (mConnectionServiceFocusManager != null) {
+ mConnectionServiceFocusManager.getHandler().removeCallbacksAndMessages(null);
+ waitForHandlerAction(mConnectionServiceFocusManager.getHandler(), TEST_TIMEOUT);
+ mConnectionServiceFocusManager.getHandler().getLooper().quit();
+ }
+
+ if (mConnectionServiceFixtureA != null) {
+ mConnectionServiceFixtureA.waitForHandlerToClear();
+ }
- mConnectionServiceFixtureA.waitForHandlerToClear();
- mConnectionServiceFixtureB.waitForHandlerToClear();
+ if (mConnectionServiceFixtureA != null) {
+ mConnectionServiceFixtureB.waitForHandlerToClear();
+ }
// Forcefully clean all sessions at the end of the test, which will also log any stale
// sessions for debugging.
@@ -411,12 +428,13 @@ public class TelecomSystemTest extends TelecomTestCase {
super.tearDown();
}
- protected ParcelableCall makeConferenceCall() throws Exception {
- IdPair callId1 = startAndMakeActiveOutgoingCall("650-555-1212",
- mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ protected ParcelableCall makeConferenceCall(
+ Intent callIntentExtras1, Intent callIntentExtras2) throws Exception {
+ IdPair callId1 = startAndMakeActiveOutgoingCallWithExtras("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA, callIntentExtras1);
- IdPair callId2 = startAndMakeActiveOutgoingCall("650-555-1213",
- mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ IdPair callId2 = startAndMakeActiveOutgoingCallWithExtras("650-555-1213",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA, callIntentExtras2);
IInCallAdapter inCallAdapter = mInCallServiceFixtureX.getInCallAdapter();
inCallAdapter.conference(callId1.mCallId, callId2.mCallId);
@@ -473,7 +491,8 @@ public class TelecomSystemTest extends TelecomTestCase {
when(mClockProxy.currentTimeMillis()).thenReturn(TEST_CREATE_TIME);
when(mClockProxy.elapsedRealtime()).thenReturn(TEST_CREATE_ELAPSED_TIME);
when(mRoleManagerAdapter.getCallCompanionApps()).thenReturn(Collections.emptyList());
- when(mRoleManagerAdapter.getDefaultCallScreeningApp()).thenReturn(null);
+ when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class)))
+ .thenReturn(null);
mTelecomSystem = new TelecomSystem(
mComponentContextFixture.getTestDouble(),
(context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter)
@@ -498,7 +517,8 @@ public class TelecomSystemTest extends TelecomTestCase {
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl) {
+ int earpieceControl,
+ Executor asyncTaskExecutor) {
return new CallAudioRouteStateMachine(context,
callsManager,
bluetoothManager,
@@ -507,7 +527,8 @@ public class TelecomSystemTest extends TelecomTestCase {
audioServiceFactory,
// Force enable an earpiece for the end-to-end tests
CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
- mHandlerThread.getLooper());
+ mHandlerThread.getLooper(),
+ Runnable::run /* async tasks as now sync for testing! */);
}
},
new CallAudioModeStateMachine.Factory() {
@@ -526,7 +547,9 @@ public class TelecomSystemTest extends TelecomTestCase {
ContactsAsyncHelper.ContentResolverAdapter adapter) {
return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
}
- }, mDeviceIdleControllerAdapter);
+ }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
+ Runnable::run,
+ mBlockedNumbersAdapter);
mComponentContextFixture.setTelecomManager(new TelecomManager(
mComponentContextFixture.getTestDouble(),
@@ -618,7 +641,7 @@ public class TelecomSystemTest extends TelecomTestCase {
startOutgoingPhoneCallWaitForBroadcaster(number, null,
connectionServiceFixture, Process.myUserHandle(), VideoProfile.STATE_AUDIO_ONLY,
- false /*isEmergency*/);
+ false /*isEmergency*/, null);
return mInCallServiceFixtureX.mLatestCallId;
}
@@ -648,17 +671,17 @@ public class TelecomSystemTest extends TelecomTestCase {
throws Exception {
return startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture,
- initiatingUser, VideoProfile.STATE_AUDIO_ONLY);
+ initiatingUser, VideoProfile.STATE_AUDIO_ONLY, null);
}
protected IdPair startOutgoingPhoneCall(String number, PhoneAccountHandle phoneAccountHandle,
ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
- int videoState) throws Exception {
+ int videoState, Intent callIntentExtras) throws Exception {
int startingNumConnections = connectionServiceFixture.mConnectionById.size();
int startingNumCalls = mInCallServiceFixtureX.mCallById.size();
startOutgoingPhoneCallPendingCreateConnection(number, phoneAccountHandle,
- connectionServiceFixture, initiatingUser, videoState);
+ connectionServiceFixture, initiatingUser, videoState, callIntentExtras);
verify(connectionServiceFixture.getTestDouble(), timeout(TEST_TIMEOUT))
.createConnectionComplete(anyString(), any());
@@ -701,7 +724,7 @@ public class TelecomSystemTest extends TelecomTestCase {
// Call will not use the ordered broadcaster, since it is an Emergency Call
startOutgoingPhoneCallWaitForBroadcaster(number, phoneAccountHandle,
- connectionServiceFixture, initiatingUser, videoState, true /*isEmergency*/);
+ connectionServiceFixture, initiatingUser, videoState, true /*isEmergency*/, null);
return outgoingCallCreateConnectionComplete(startingNumConnections, startingNumCalls,
phoneAccountHandle, connectionServiceFixture);
@@ -710,7 +733,7 @@ public class TelecomSystemTest extends TelecomTestCase {
protected void startOutgoingPhoneCallWaitForBroadcaster(String number,
PhoneAccountHandle phoneAccountHandle,
ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
- int videoState, boolean isEmergency) throws Exception {
+ int videoState, boolean isEmergency, Intent actionCallIntent) throws Exception {
reset(connectionServiceFixture.getTestDouble(), mInCallServiceFixtureX.getTestDouble(),
mInCallServiceFixtureY.getTestDouble());
@@ -723,7 +746,9 @@ public class TelecomSystemTest extends TelecomTestCase {
boolean hasInCallAdapter = mInCallServiceFixtureX.mInCallAdapter != null;
- Intent actionCallIntent = new Intent();
+ if (actionCallIntent == null) {
+ actionCallIntent = new Intent();
+ }
actionCallIntent.setData(Uri.parse("tel:" + number));
actionCallIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
if(isEmergency) {
@@ -743,7 +768,7 @@ public class TelecomSystemTest extends TelecomTestCase {
final UserHandle userHandle = initiatingUser;
Context localAppContext = mComponentContextFixture.getTestDouble().getApplicationContext();
new UserCallIntentProcessor(localAppContext, userHandle).processIntent(
- actionCallIntent, null, true /* hasCallAppOp*/, false /* isLocal */);
+ actionCallIntent, null, false, true /* hasCallAppOp*/, false /* isLocal */);
// Wait for handler to start CallerInfo lookup.
waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
// Send the CallerInfo lookup reply.
@@ -768,9 +793,10 @@ public class TelecomSystemTest extends TelecomTestCase {
protected String startOutgoingPhoneCallPendingCreateConnection(String number,
PhoneAccountHandle phoneAccountHandle,
ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
- int videoState) throws Exception {
+ int videoState, Intent callIntentExtras) throws Exception {
startOutgoingPhoneCallWaitForBroadcaster(number,phoneAccountHandle,
- connectionServiceFixture, initiatingUser, videoState, false /*isEmergency*/);
+ connectionServiceFixture, initiatingUser,
+ videoState, false /*isEmergency*/, callIntentExtras);
waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
verifyAndProcessOutgoingCallBroadcast(phoneAccountHandle);
@@ -875,14 +901,24 @@ public class TelecomSystemTest extends TelecomTestCase {
PhoneAccountHandle phoneAccountHandle,
final ConnectionServiceFixture connectionServiceFixture) throws Exception {
return startIncomingPhoneCall(number, phoneAccountHandle, VideoProfile.STATE_AUDIO_ONLY,
- connectionServiceFixture);
+ connectionServiceFixture, null);
+ }
+
+ protected IdPair startIncomingPhoneCallWithExtras(
+ String number,
+ PhoneAccountHandle phoneAccountHandle,
+ final ConnectionServiceFixture connectionServiceFixture,
+ Bundle extras) throws Exception {
+ return startIncomingPhoneCall(number, phoneAccountHandle, VideoProfile.STATE_AUDIO_ONLY,
+ connectionServiceFixture, extras);
}
protected IdPair startIncomingPhoneCall(
String number,
PhoneAccountHandle phoneAccountHandle,
int videoState,
- final ConnectionServiceFixture connectionServiceFixture) throws Exception {
+ final ConnectionServiceFixture connectionServiceFixture,
+ Bundle extras) throws Exception {
reset(connectionServiceFixture.getTestDouble(), mInCallServiceFixtureX.getTestDouble(),
mInCallServiceFixtureY.getTestDouble());
@@ -899,12 +935,14 @@ public class TelecomSystemTest extends TelecomTestCase {
new IncomingCallAddedListener(incomingCallAddedLatch);
mTelecomSystem.getCallsManager().addListener(callAddedListener);
- Bundle extras = new Bundle();
+ if (extras == null) {
+ extras = new Bundle();
+ }
extras.putParcelable(
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null));
mTelecomSystem.getTelecomServiceImpl().getBinder()
- .addNewIncomingCall(phoneAccountHandle, extras);
+ .addNewIncomingCall(phoneAccountHandle, extras, CALLING_PACKAGE);
verify(connectionServiceFixture.getTestDouble())
.createConnection(any(PhoneAccountHandle.class), anyString(),
@@ -990,7 +1028,16 @@ public class TelecomSystemTest extends TelecomTestCase {
PhoneAccountHandle phoneAccountHandle,
ConnectionServiceFixture connectionServiceFixture) throws Exception {
return startAndMakeActiveOutgoingCall(number, phoneAccountHandle, connectionServiceFixture,
- VideoProfile.STATE_AUDIO_ONLY);
+ VideoProfile.STATE_AUDIO_ONLY, null);
+ }
+
+ protected IdPair startAndMakeActiveOutgoingCallWithExtras(
+ String number,
+ PhoneAccountHandle phoneAccountHandle,
+ ConnectionServiceFixture connectionServiceFixture,
+ Intent callIntentExtras) throws Exception {
+ return startAndMakeActiveOutgoingCall(number, phoneAccountHandle, connectionServiceFixture,
+ VideoProfile.STATE_AUDIO_ONLY, callIntentExtras);
}
// A simple outgoing call, verifying that the appropriate connection service is contacted,
@@ -998,9 +1045,10 @@ public class TelecomSystemTest extends TelecomTestCase {
protected IdPair startAndMakeActiveOutgoingCall(
String number,
PhoneAccountHandle phoneAccountHandle,
- ConnectionServiceFixture connectionServiceFixture, int videoState) throws Exception {
+ ConnectionServiceFixture connectionServiceFixture, int videoState,
+ Intent callIntentExtras) throws Exception {
IdPair ids = startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture,
- Process.myUserHandle(), videoState);
+ Process.myUserHandle(), videoState, callIntentExtras);
connectionServiceFixture.sendSetDialing(ids.mConnectionId);
if (phoneAccountHandle != mPhoneAccountSelfManaged.getAccountHandle()) {
@@ -1109,7 +1157,7 @@ public class TelecomSystemTest extends TelecomTestCase {
PhoneAccountHandle phoneAccountHandle,
ConnectionServiceFixture connectionServiceFixture) throws Exception {
IdPair ids = startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture,
- Process.myUserHandle(), VideoProfile.STATE_AUDIO_ONLY);
+ Process.myUserHandle(), VideoProfile.STATE_AUDIO_ONLY, null);
connectionServiceFixture.sendSetDialing(ids.mConnectionId);
if (phoneAccountHandle != mPhoneAccountSelfManaged.getAccountHandle()) {
diff --git a/tests/src/com/android/server/telecom/tests/TestScheduledExecutorService.java b/tests/src/com/android/server/telecom/tests/TestScheduledExecutorService.java
new file mode 100644
index 000000000..22a0be19b
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TestScheduledExecutorService.java
@@ -0,0 +1,283 @@
+/*
+ * 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 android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A test implementation of a scheduled executor service.
+ */
+public class TestScheduledExecutorService implements ScheduledExecutorService {
+ private static final String TAG = "TestScheduledExecutorService";
+
+ private class CompletedFuture<T> implements Future<T>, ScheduledFuture<T> {
+
+ private final Callable<T> mTask;
+ private final long mDelayMs;
+ private Runnable mRunnable;
+
+ CompletedFuture(Callable<T> task) {
+ mTask = task;
+ mDelayMs = 0;
+ }
+
+ @SuppressWarnings("unused")
+ CompletedFuture(Callable<T> task, long delayMs) {
+ mTask = task;
+ mDelayMs = delayMs;
+ }
+
+ CompletedFuture(Runnable task, long delayMs) {
+ mRunnable = task;
+ mTask = (Callable<T>) Executors.callable(task);
+ mDelayMs = delayMs;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ cancelRunnable(mRunnable);
+ return true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ try {
+ return mTask.call();
+ } catch (Exception e) {
+ throw new ExecutionException(e);
+ }
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ try {
+ return mTask.call();
+ } catch (Exception e) {
+ throw new ExecutionException(e);
+ }
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ if (unit == TimeUnit.MILLISECONDS) {
+ return mDelayMs;
+ } else {
+ // not implemented
+ return 0;
+ }
+ }
+
+ @Override
+ public int compareTo(Delayed o) {
+ if (o == null) return 1;
+ if (o.getDelay(TimeUnit.MILLISECONDS) > mDelayMs) return -1;
+ if (o.getDelay(TimeUnit.MILLISECONDS) < mDelayMs) return 1;
+ return 0;
+ }
+ }
+
+ private long mClock = 0;
+ private Map<Long, Runnable> mScheduledRunnables = new HashMap<>();
+ private Map<Runnable, Long> mRepeatDuration = new HashMap<>();
+
+ @Override
+ public void shutdown() {
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ return null;
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return false;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return false;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) {
+ return false;
+ }
+
+ @Override
+ public <T> Future<T> submit(Callable<T> task) {
+ return new TestScheduledExecutorService.CompletedFuture<>(task);
+ }
+
+ @Override
+ public <T> Future<T> submit(Runnable task, T result) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Future<?> submit(Runnable task) {
+ task.run();
+ return new TestScheduledExecutorService.CompletedFuture<>(() -> null);
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,
+ TimeUnit unit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ // Schedule the runnable for execution at the specified time.
+ long scheduledTime = getNextExecutionTime(delay, unit);
+ mScheduledRunnables.put(scheduledTime, command);
+
+ Log.i(TAG, "schedule: runnable=" + System.identityHashCode(command) + ", time="
+ + scheduledTime);
+
+ return new TestScheduledExecutorService.CompletedFuture<Runnable>(command, delay);
+ }
+
+ @Override
+ public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,
+ TimeUnit unit) {
+ return scheduleWithFixedDelay(command, initialDelay, period, unit);
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
+ long delay, TimeUnit unit) {
+ // Schedule the runnable for execution at the specified time.
+ long nextScheduledTime = getNextExecutionTime(delay, unit);
+ mScheduledRunnables.put(nextScheduledTime, command);
+ mRepeatDuration.put(command, unit.toMillis(delay));
+
+ return new TestScheduledExecutorService.CompletedFuture<Runnable>(command, delay);
+ }
+
+ private long getNextExecutionTime(long delay, TimeUnit unit) {
+ long delayMillis = unit.toMillis(delay);
+ return mClock + delayMillis;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+
+ /**
+ * Used in unit tests, used to add a delta to the "clock" so that we can fire off scheduled
+ * items and reschedule the repeats.
+ * @param duration The duration (millis) to add to the clock.
+ */
+ public void advanceTime(long duration) {
+ Map<Long, Runnable> nextRepeats = new HashMap<>();
+ List<Runnable> toRun = new ArrayList<>();
+ mClock += duration;
+ Iterator<Map.Entry<Long, Runnable>> iterator = mScheduledRunnables.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<Long, Runnable> entry = iterator.next();
+ if (mClock >= entry.getKey()) {
+ toRun.add(entry.getValue());
+
+ Runnable r = entry.getValue();
+ Log.i(TAG, "advanceTime: runningRunnable=" + System.identityHashCode(r));
+ // If this is a repeating scheduled item, schedule the repeat.
+ if (mRepeatDuration.containsKey(r)) {
+ // schedule next execution
+ nextRepeats.put(mClock + mRepeatDuration.get(r), entry.getValue());
+ }
+ iterator.remove();
+ }
+ }
+
+ // Update things at the end to avoid concurrent access.
+ mScheduledRunnables.putAll(nextRepeats);
+ toRun.forEach(r -> r.run());
+ }
+
+ /**
+ * Used from a {@link CompletedFuture} as defined above to cancel a scheduled task.
+ * @param r The runnable to cancel.
+ */
+ private void cancelRunnable(Runnable r) {
+ Optional<Map.Entry<Long, Runnable>> found = mScheduledRunnables.entrySet().stream()
+ .filter(e -> e.getValue() == r)
+ .findFirst();
+ if (found.isPresent()) {
+ mScheduledRunnables.remove(found.get().getKey());
+ }
+ mRepeatDuration.remove(r);
+ Log.i(TAG, "cancelRunnable: runnable=" + System.identityHashCode(r));
+ }
+
+ public int getNumberOfScheduledRunnables() {
+ return mScheduledRunnables.size();
+ }
+
+ public boolean isRunnableScheduledAtTime(long time) {
+ return mScheduledRunnables.containsKey(time);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
new file mode 100644
index 000000000..3fc87a9fa
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -0,0 +1,283 @@
+/*
+ * 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.mockito.ArgumentMatchers.eq;
+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.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.UserHandle;
+import android.telecom.CallAttributes;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+
+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.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelecomSystem;
+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;
+
+
+public class TransactionTests extends TelecomTestCase {
+
+ private static final String CALL_ID_1 = "1";
+
+ private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
+ new ComponentName("foo", "bar"), "1");
+ private static final String TEST_NAME = "Sergey Brin";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+
+ @Mock private Call mMockCall1;
+ @Mock private Context mMockContext;
+ @Mock private CallsManager mCallsManager;
+ @Mock private ToastFactory mToastFactory;
+ @Mock private ClockProxy mClockProxy;
+ @Mock private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+ @Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
+
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {
+ };
+ private static final Uri TEST_ADDRESS = Uri.parse("tel:555-1212");
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testEndCallTransactionWithDisconnect() throws Exception {
+ // GIVEN
+ EndCallTransaction transaction =
+ new EndCallTransaction(mCallsManager, new DisconnectCause(0), mMockCall1);
+
+ // WHEN
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .markCallAsDisconnected(mMockCall1, new DisconnectCause(0));
+ verify(mCallsManager, never())
+ .rejectCall(mMockCall1, 0);
+ verify(mCallsManager, times(1))
+ .markCallAsRemoved(mMockCall1);
+ }
+
+ @Test
+ public void testHoldCallTransaction() throws Exception {
+ // GIVEN
+ Call spyCall = createSpyCall(null, CallState.ACTIVE, CALL_ID_1);
+
+ HoldCallTransaction transaction =
+ new HoldCallTransaction(mCallsManager, spyCall);
+
+ // WHEN
+ when(mCallsManager.canHold(spyCall)).thenReturn(true);
+ doAnswer(invocation -> {
+ Call call = invocation.getArgument(0);
+ call.setState(CallState.ON_HOLD, "manual set");
+ return null;
+ }).when(mCallsManager).markCallAsOnHold(spyCall);
+
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .markCallAsOnHold(spyCall);
+
+ assertEquals(CallState.ON_HOLD, spyCall.getState());
+ }
+
+ @Test
+ public void testRequestNewCallFocusWithDialingCall() throws Exception {
+ // GIVEN
+ RequestNewActiveCallTransaction transaction =
+ new RequestNewActiveCallTransaction(mCallsManager, mMockCall1);
+
+ // WHEN
+ when(mMockCall1.getState()).thenReturn(CallState.DIALING);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .requestNewCallFocusAndVerify(eq(mMockCall1), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testRequestNewCallFocusWithRingingCall() throws Exception {
+ // GIVEN
+ RequestNewActiveCallTransaction transaction =
+ new RequestNewActiveCallTransaction(mCallsManager, mMockCall1);
+
+ // WHEN
+ when(mMockCall1.getState()).thenReturn(CallState.RINGING);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .requestNewCallFocusAndVerify(eq(mMockCall1), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testRequestNewCallFocusFailure() throws Exception {
+ // GIVEN
+ RequestNewActiveCallTransaction transaction =
+ new RequestNewActiveCallTransaction(mCallsManager, mMockCall1);
+
+ // WHEN
+ when(mMockCall1.getState()).thenReturn(CallState.DISCONNECTING);
+ when(mCallsManager.getActiveCall()).thenReturn(null);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(0))
+ .requestNewCallFocusAndVerify( eq(mMockCall1), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testTransactionalHoldActiveCallForNewCall() throws Exception {
+ // GIVEN
+ MaybeHoldCallForNewCallTransaction transaction =
+ new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1);
+
+ // WHEN
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1),
+ isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testOutgoingCallTransaction() throws Exception {
+ // GIVEN
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+
+ OutgoingCallTransaction transaction =
+ new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+
+ // WHEN
+ when(mMockContext.getOpPackageName()).thenReturn("testPackage");
+ when(mMockContext.checkCallingPermission(android.Manifest.permission.CALL_PRIVILEGED))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mCallsManager.isOutgoingCallPermitted(callAttributes.getPhoneAccountHandle()))
+ .thenReturn(true);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .startOutgoingCall(isA(Uri.class),
+ isA(PhoneAccountHandle.class),
+ isA(Bundle.class),
+ isA(UserHandle.class),
+ isA(Intent.class),
+ nullable(String.class));
+ }
+
+ @Test
+ public void testIncomingCallTransaction() throws Exception {
+ // GIVEN
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
+
+ IncomingCallTransaction transaction =
+ new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+
+ // WHEN
+ when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
+ .thenReturn(true);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .processIncomingCallIntent(isA(PhoneAccountHandle.class),
+ isA(Bundle.class),
+ isA(Boolean.class));
+ }
+
+ private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) {
+ when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
+
+ Call call = new Call(id,
+ mMockContext,
+ mCallsManager,
+ mLock, /* ConnectionServiceRepository */
+ null,
+ mPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null /* ConnectionManagerAccount */,
+ targetPhoneAccount,
+ Call.CALL_DIRECTION_INCOMING,
+ false /* shouldAttachToExistingConnection*/,
+ false /* isConference */,
+ mClockProxy,
+ mToastFactory);
+
+ Call callSpy = Mockito.spy(call);
+
+ callSpy.setState(initialState, "manual set in test");
+
+ // Mocks some methods to not call the real method.
+ doNothing().when(callSpy).unhold();
+ doNothing().when(callSpy).hold();
+ doNothing().when(callSpy).disconnect();
+
+ return callSpy;
+ }
+} \ 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
new file mode 100644
index 000000000..fa5f2a295
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.isA;
+
+
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.telecom.ICallControl;
+import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+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 org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class TransactionalServiceWrapperTest extends TelecomTestCase {
+
+ private static final PhoneAccountHandle SERVICE_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.foo/.Blah"), "Service1");
+
+ private static final String CALL_ID_1 = "1";
+ private static final String CALL_ID_2 = "2";
+
+ TransactionalServiceWrapper mTransactionalServiceWrapper;
+
+ @Mock private Call mMockCall1;
+ @Mock private Call mMockCall2;
+ @Mock private CallsManager mCallsManager;
+ @Mock private TransactionManager mTransactionManager;
+ @Mock private ICallEventCallback mCallEventCallback;
+ @Mock private TransactionalServiceRepository mRepository;
+ @Mock private IBinder mIBinder;
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ Mockito.when(mMockCall2.getId()).thenReturn(CALL_ID_2);
+ Mockito.when(mCallsManager.getLock()).thenReturn(mLock);
+ Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
+ mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+
+ mTransactionalServiceWrapper.setTransactionManager(mTransactionManager);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testTransactionalServiceWrapperStartState() throws Exception {
+ TransactionalServiceWrapper service =
+ new TransactionalServiceWrapper(mCallEventCallback,
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+
+ assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
+ assertEquals(1, service.getNumberOfTrackedCalls());
+ }
+
+ @Test
+ public void testTransactionalServiceWrapperCallCount() throws Exception {
+ TransactionalServiceWrapper service =
+ new TransactionalServiceWrapper(mCallEventCallback,
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+
+ assertEquals(1, service.getNumberOfTrackedCalls());
+ service.trackCall(mMockCall2);
+ assertEquals(2, service.getNumberOfTrackedCalls());
+
+ assertTrue(service.untrackCall(mMockCall2));
+ assertEquals(1, service.getNumberOfTrackedCalls());
+
+ assertTrue(service.untrackCall(mMockCall1));
+ assertFalse(service.untrackCall(mMockCall1));
+ assertEquals(0, service.getNumberOfTrackedCalls());
+ }
+
+ @Test
+ public void testCallControlSetActive() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.setActive(CALL_ID_1, new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(SerialTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testCallControlRejectCall() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.disconnect(CALL_ID_1, new DisconnectCause(DisconnectCause.REJECTED),
+ new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(EndCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testCallControlDisconnectCall() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.disconnect(CALL_ID_1, new DisconnectCause(DisconnectCause.LOCAL),
+ new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(EndCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testCallControlSetInactive() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.setInactive(CALL_ID_1, new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(HoldCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
index 97e71d18b..84beedc0f 100644
--- a/tests/src/com/android/server/telecom/tests/VideoCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
@@ -105,7 +105,7 @@ public class VideoCallTests extends TelecomSystemTest {
// Start an incoming video call.
IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
- VideoProfile.STATE_BIDIRECTIONAL);
+ VideoProfile.STATE_BIDIRECTIONAL, null);
verifyAudioRoute(CallAudioState.ROUTE_SPEAKER);
}
@@ -121,7 +121,7 @@ public class VideoCallTests extends TelecomSystemTest {
// Start an incoming video call.
IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
- VideoProfile.STATE_TX_ENABLED);
+ VideoProfile.STATE_TX_ENABLED, null);
verifyAudioRoute(CallAudioState.ROUTE_SPEAKER);
}
@@ -137,7 +137,7 @@ public class VideoCallTests extends TelecomSystemTest {
// Start an incoming video call.
IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
- VideoProfile.STATE_AUDIO_ONLY);
+ VideoProfile.STATE_AUDIO_ONLY, null);
verifyAudioRoute(CallAudioState.ROUTE_EARPIECE);
}
@@ -165,7 +165,7 @@ public class VideoCallTests extends TelecomSystemTest {
@Test
public void testIncomingVideoCallMissedCheckVideoHistory() throws Exception {
IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
@@ -182,7 +182,7 @@ public class VideoCallTests extends TelecomSystemTest {
@Test
public void testIncomingVideoCallRejectedCheckVideoHistory() throws Exception {
IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
@@ -201,7 +201,7 @@ public class VideoCallTests extends TelecomSystemTest {
public void testOutgoingVideoCallCanceledCheckVideoHistory() throws Exception {
IdPair ids = startOutgoingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
mConnectionServiceFixtureA, Process.myUserHandle(),
- VideoProfile.STATE_BIDIRECTIONAL);
+ VideoProfile.STATE_BIDIRECTIONAL, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
@@ -219,7 +219,7 @@ public class VideoCallTests extends TelecomSystemTest {
public void testOutgoingVideoCallRejectedCheckVideoHistory() throws Exception {
IdPair ids = startOutgoingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
mConnectionServiceFixtureA, Process.myUserHandle(),
- VideoProfile.STATE_BIDIRECTIONAL);
+ VideoProfile.STATE_BIDIRECTIONAL, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
@@ -237,7 +237,7 @@ public class VideoCallTests extends TelecomSystemTest {
public void testOutgoingVideoCallAnsweredAsAudio() throws Exception {
IdPair ids = startOutgoingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
mConnectionServiceFixtureA, Process.myUserHandle(),
- VideoProfile.STATE_BIDIRECTIONAL);
+ VideoProfile.STATE_BIDIRECTIONAL, null);
com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
.iterator().next();
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
new file mode 100644
index 000000000..c66b0f715
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -0,0 +1,304 @@
+/*
+ * 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.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+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.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.telecom.PhoneAccountHandle;
+import android.test.suitebuilder.annotation.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 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;
+
+@RunWith(JUnit4.class)
+public class VoipCallMonitorTest extends TelecomTestCase {
+ private VoipCallMonitor mMonitor;
+ private static final String NAME = "John Smith";
+ private static final String PKG_NAME_1 = "telecom.voip.test1";
+ private static final String PKG_NAME_2 = "telecom.voip.test2";
+ private static final String CLS_NAME = "VoipActivity";
+ private static final String ID_1 = "id1";
+ public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
+ private static final UserHandle USER_HANDLE_1 = new UserHandle(1);
+ private static final long TIMEOUT = 5000L;
+
+ @Mock private TelecomSystem.SyncRoot mLock;
+ @Mock private ActivityManagerInternal mActivityManagerInternal;
+ @Mock private IBinder mServiceConnection;
+
+ private final PhoneAccountHandle mHandle1User1 = new PhoneAccountHandle(
+ new ComponentName(PKG_NAME_1, CLS_NAME), ID_1, USER_HANDLE_1);
+ private final PhoneAccountHandle mHandle2User1 = new PhoneAccountHandle(
+ new ComponentName(PKG_NAME_2, CLS_NAME), ID_1, USER_HANDLE_1);
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mMonitor = new VoipCallMonitor(mContext, mLock);
+ mActivityManagerInternal = mock(ActivityManagerInternal.class);
+ mMonitor.setActivityManagerInternal(mActivityManagerInternal);
+ mMonitor.startMonitor();
+ when(mActivityManagerInternal.startForegroundServiceDelegate(any(
+ ForegroundServiceDelegationOptions.class), any(ServiceConnection.class)))
+ .thenReturn(true);
+ }
+
+ @SmallTest
+ @Test
+ public void testStartMonitorForOneCall() {
+ Call call = createTestCall("testCall", mHandle1User1);
+ IBinder service = mock(IBinder.class);
+
+ ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
+ mMonitor.onCallAdded(call);
+ verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(any(
+ ForegroundServiceDelegationOptions.class), captor.capture());
+ ServiceConnection conn = captor.getValue();
+ conn.onServiceConnected(mHandle1User1.getComponentName(), service);
+
+ mMonitor.onCallRemoved(call);
+ verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(eq(conn));
+ }
+
+ @SmallTest
+ @Test
+ public void testMonitorForTwoCallsOnSameHandle() {
+ Call call1 = createTestCall("testCall1", mHandle1User1);
+ Call call2 = createTestCall("testCall2", mHandle1User1);
+ IBinder service = mock(IBinder.class);
+
+ ArgumentCaptor<ServiceConnection> captor1 =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ mMonitor.onCallAdded(call1);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
+ captor1.capture());
+ ServiceConnection conn1 = captor1.getValue();
+ conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
+
+ ArgumentCaptor<ServiceConnection> captor2 =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ mMonitor.onCallAdded(call2);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
+ .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
+ captor2.capture());
+ ServiceConnection conn2 = captor2.getValue();
+ conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
+
+ mMonitor.onCallRemoved(call1);
+ verify(mActivityManagerInternal, never()).stopForegroundServiceDelegate(
+ any(ServiceConnection.class));
+ mMonitor.onCallRemoved(call2);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .stopForegroundServiceDelegate(eq(conn2));
+ }
+
+ @SmallTest
+ @Test
+ public void testMonitorForTwoCallsOnDifferentHandle() {
+ Call call1 = createTestCall("testCall1", mHandle1User1);
+ Call call2 = createTestCall("testCall2", mHandle2User1);
+ IBinder service = mock(IBinder.class);
+
+ ArgumentCaptor<ServiceConnection> connCaptor1 = ArgumentCaptor.forClass(
+ ServiceConnection.class);
+ ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor1 =
+ ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+ mMonitor.onCallAdded(call1);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .startForegroundServiceDelegate(optionsCaptor1.capture(), connCaptor1.capture());
+ ForegroundServiceDelegationOptions options1 = optionsCaptor1.getValue();
+ ServiceConnection conn1 = connCaptor1.getValue();
+ conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
+ assertEquals(PKG_NAME_1, options1.getComponentName().getPackageName());
+
+ ArgumentCaptor<ServiceConnection> connCaptor2 = ArgumentCaptor.forClass(
+ ServiceConnection.class);
+ ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor2 =
+ ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+ mMonitor.onCallAdded(call2);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
+ .startForegroundServiceDelegate(optionsCaptor2.capture(), connCaptor2.capture());
+ ForegroundServiceDelegationOptions options2 = optionsCaptor2.getValue();
+ ServiceConnection conn2 = connCaptor2.getValue();
+ conn2.onServiceConnected(mHandle2User1.getComponentName(), service);
+ assertEquals(PKG_NAME_2, options2.getComponentName().getPackageName());
+
+ mMonitor.onCallRemoved(call2);
+ verify(mActivityManagerInternal).stopForegroundServiceDelegate(eq(conn2));
+ mMonitor.onCallRemoved(call1);
+ verify(mActivityManagerInternal).stopForegroundServiceDelegate(eq(conn1));
+ }
+
+ @SmallTest
+ @Test
+ public void testStopDelegation() {
+ Call call1 = createTestCall("testCall1", mHandle1User1);
+ Call call2 = createTestCall("testCall2", mHandle1User1);
+ IBinder service = mock(IBinder.class);
+
+ ArgumentCaptor<ServiceConnection> captor1 =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ mMonitor.onCallAdded(call1);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
+ captor1.capture());
+ ServiceConnection conn1 = captor1.getValue();
+ conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
+
+ ArgumentCaptor<ServiceConnection> captor2 =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ mMonitor.onCallAdded(call2);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
+ .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
+ captor2.capture());
+ ServiceConnection conn2 = captor2.getValue();
+ conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
+
+ mMonitor.stopFGSDelegation(call1);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .stopForegroundServiceDelegate(eq(conn2));
+ conn2.onServiceDisconnected(mHandle1User1.getComponentName());
+ mMonitor.onCallRemoved(call1);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .stopForegroundServiceDelegate(any(ServiceConnection.class));
+ }
+
+ /**
+ * Ensure an app loses foreground service delegation if the user dismisses the call style
+ * notification or the app removes the notification.
+ * Note: post the notification AFTER foreground service delegation is gained
+ */
+ @SmallTest
+ @Test
+ public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() {
+ // GIVEN
+ StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+
+ // WHEN
+ // FGS is gained after the call is added to VoipCallMonitor
+ ServiceConnection c = addCallAndVerifyFgsIsGained(createTestCall("1", mHandle1User1));
+ // simulate an app posting a call style notification after FGS is gained
+ mMonitor.postNotification(sbn);
+
+ // THEN
+ // shortly after posting the notification, simulate the user dismissing it
+ mMonitor.removeNotification(sbn);
+ // FGS should be removed once the notification is removed
+ verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c);
+ }
+
+ /**
+ * Ensure an app loses foreground service delegation if the user dismisses the call style
+ * notification or the app removes the notification.
+ * Note: post the notification BEFORE foreground service delegation is gained
+ */
+ @SmallTest
+ @Test
+ public void testStopFgsIfCallNotificationIsRemoved_PostedBeforeFgsIsGained() {
+ // GIVEN
+ StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+
+ // WHEN
+ // an app posts a call style notification before FGS is gained
+ mMonitor.postNotification(sbn);
+ // FGS is gained after the call is added to VoipCallMonitor
+ ServiceConnection c = addCallAndVerifyFgsIsGained(createTestCall("1", mHandle1User1));
+
+ // THEN
+ // shortly after posting the notification, simulate the user dismissing it
+ mMonitor.removeNotification(sbn);
+ // FGS should be removed once the notification is removed
+ verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c);
+ }
+
+ private Call createTestCall(String id, PhoneAccountHandle handle) {
+ Call call = mock(Call.class);
+ when(call.getTargetPhoneAccount()).thenReturn(handle);
+ when(call.isTransactionalCall()).thenReturn(true);
+ when(call.getExtras()).thenReturn(new Bundle());
+ when(call.getId()).thenReturn(id);
+ when(call.getCallingPackageIdentity()).thenReturn(new Call.CallingPackageIdentity());
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ return call;
+ }
+
+ private Notification createCallStyleNotification() {
+ PendingIntent pendingOngoingIntent = PendingIntent.getActivity(mContext, 0,
+ new Intent(""), PendingIntent.FLAG_IMMUTABLE);
+
+ return new Notification.Builder(mContext,
+ CHANNEL_ID)
+ .setStyle(Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName(NAME).setImportant(true).build(),
+ pendingOngoingIntent)
+ )
+ .setFullScreenIntent(pendingOngoingIntent, true)
+ .build();
+ }
+
+ private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) {
+ return new StatusBarNotification(
+ handle.getComponentName().getPackageName(), "", 0, "", 0, 0,
+ createCallStyleNotification(), handle.getUserHandle(), "", 0);
+ }
+
+ private ServiceConnection addCallAndVerifyFgsIsGained(Call call) {
+ ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
+ // add the call to the VoipCallMonitor under test which will start FGS
+ mMonitor.onCallAdded(call);
+ // FGS should be granted within the timeout
+ verify(mActivityManagerInternal, timeout(TIMEOUT))
+ .startForegroundServiceDelegate(any(
+ ForegroundServiceDelegationOptions.class),
+ captor.capture());
+ // onServiceConnected must be called in order for VoipCallMonitor to start monitoring for
+ // a notification before the timeout expires
+ ServiceConnection serviceConnection = captor.getValue();
+ serviceConnection.onServiceConnected(mHandle1User1.getComponentName(), mServiceConnection);
+ return serviceConnection;
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
new file mode 100644
index 000000000..0a7e27d50
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+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 org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(JUnit4.class)
+public class VoipCallTransactionTest extends TelecomTestCase {
+ private StringBuilder mLog;
+ private TransactionManager mTransactionManager;
+ private static final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
+
+ private class TestVoipCallTransaction extends VoipCallTransaction {
+ public static final int SUCCESS = 0;
+ public static final int FAILED = 1;
+ public static final int TIMEOUT = 2;
+
+ private long mSleepTime;
+ private String mName;
+ private int mType;
+
+ public TestVoipCallTransaction(String name, long sleepTime, int type) {
+ super(VoipCallTransactionTest.this.mLock);
+ mName = name;
+ mSleepTime = sleepTime;
+ mType = type;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
+ mHandler.postDelayed(() -> {
+ if (mType == SUCCESS) {
+ mLog.append(mName).append(" success;\n");
+ resultFuture.complete(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ null));
+ } else if (mType == FAILED) {
+ mLog.append(mName).append(" failed;\n");
+ resultFuture.complete(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ null));
+ } else {
+ mLog.append(mName).append(" timeout;\n");
+ resultFuture.complete(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+ "timeout"));
+ }
+ }, mSleepTime);
+ return resultFuture;
+ }
+ }
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mTransactionManager = TransactionManager.getTestInstance();
+ mLog = new StringBuilder();
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ Log.i("Grace", mLog.toString());
+ mTransactionManager.clear();
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testSerialTransactionSuccess()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ List<VoipCallTransaction> subTransactions = new ArrayList<>();
+ VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ VoipCallTransaction 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 =
+ resultFuture::complete;
+ String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
+ mTransactionManager.addTransaction(new SerialTransaction(subTransactions, mLock),
+ outcomeReceiver);
+ assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+ resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
+ assertEquals(expectedLog, mLog.toString());
+ }
+
+ @SmallTest
+ @Test
+ public void testSerialTransactionFailed()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ List<VoipCallTransaction> subTransactions = new ArrayList<>();
+ VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+ TestVoipCallTransaction.FAILED);
+ VoipCallTransaction 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>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+
+ }
+
+ @Override
+ public void onError(CallException e) {
+ exceptionFuture.complete(e.getMessage());
+ }
+ };
+ mTransactionManager.addTransaction(new SerialTransaction(subTransactions, mLock),
+ outcomeReceiver);
+ exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
+ String expectedLog = "t1 success;\nt2 failed;\n";
+ assertEquals(expectedLog, mLog.toString());
+ }
+
+ @SmallTest
+ @Test
+ public void testParallelTransactionSuccess()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ List<VoipCallTransaction> subTransactions = new ArrayList<>();
+ VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+ TestVoipCallTransaction.SUCCESS);
+ VoipCallTransaction 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 =
+ resultFuture::complete;
+ mTransactionManager.addTransaction(new ParallelTransaction(subTransactions, mLock),
+ outcomeReceiver);
+ assertEquals(VoipCallTransactionResult.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"));
+ }
+
+ @SmallTest
+ @Test
+ public void testParallelTransactionFailed()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ List<VoipCallTransaction> subTransactions = new ArrayList<>();
+ VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+ TestVoipCallTransaction.SUCCESS);
+ VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+ TestVoipCallTransaction.FAILED);
+ VoipCallTransaction 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 =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+
+ }
+
+ @Override
+ public void onError(CallException e) {
+ exceptionFuture.complete(e.getMessage());
+ }
+ };
+ mTransactionManager.addTransaction(new ParallelTransaction(subTransactions, mLock),
+ outcomeReceiver);
+ exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
+ assertTrue(mLog.toString().contains("t2 failed;\n"));
+ }
+
+ @SmallTest
+ @Test
+ public void testTransactionTimeout()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
+ TestVoipCallTransaction.SUCCESS);
+ CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+
+ }
+
+ @Override
+ public void onError(CallException e) {
+ exceptionFuture.complete(e.getMessage());
+ }
+ }; mTransactionManager.addTransaction(t, outcomeReceiver);
+ String message = exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
+ assertTrue(message.contains("timeout"));
+ }
+}