[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 5f532cfecf -s ours
am skip reason: subject contains skip directive
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/23133061
Change-Id: I66175811905dddd7b188ed0acdf1178db1835516
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 1b422aa..c5141ca 100644
--- a/Android.bp
+++ b/Android.bp
@@ -28,6 +28,9 @@
static_libs: [
"androidx.annotation_annotation",
],
+ libs: [
+ "services",
+ ],
resource_dirs: ["res"],
proto: {
type: "nano",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d122ff8..d42dcff 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -64,6 +64,7 @@
<uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<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 +138,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>
@@ -277,7 +279,7 @@
<activity android:name=".settings.EnableAccountPreferenceActivity"
android:label="@string/enable_account_preference_title"
android:configChanges="orientation|screenSize|keyboardHidden"
- android:theme="@style/Theme.Telecom.DialerSettings"
+ android:theme="@style/Theme.Telecom.EnableAccount"
android:process=":ui"
android:exported="true">
<intent-filter>
diff --git a/OWNERS b/OWNERS
index 39be2c1..97cc81f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,6 +1,8 @@
breadley@google.com
-hallliu@google.com
tgunn@google.com
xiaotonj@google.com
-shuoq@google.com
+chinmayd@google.com
+tjstuart@google.com
rgreenwalt@google.com
+pmadapurmath@google.com
+grantmenke@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 9874044..489fab7 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/values-af/strings.xml b/res/values-af/strings.xml
index a4b41db..61381ae 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 3e91608..79f29bf 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index fe39603..b9f8842 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 004e62e..75d3416 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index c8e403b..d2368fa 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 1a8b1fb..f77b0bb 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 8fc4be7..8560c9c 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index f67820d..c99dcd0 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 800db09..01b67f0 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index d8708d1..201d8d1 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 45f182c..2c5727d 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 9238421..2945d28 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 9e755ad..366b584 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 659ff91..801321b 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 91d94d4..7a09f0a 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 52a1c64..0249401 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 55df474..5f857c1 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 52a1c64..0249401 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 52a1c64..0249401 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index e5d1332..2ffae87 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 4c6fe21..ab8f454 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 1e5866a..65ab627 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 46d36bc..7d9ad7b 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index ae9aa26..64645a4 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 11f5491..83c8034 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 30809ed..4ade7d1 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 789909a..95b2069 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 75882d1..03f6d87 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 23fa036..a8443dd 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 2b2ad64..4af6351 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -25,7 +25,7 @@
<string name="notification_missedCallsMsg" msgid="5055782736170916682">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ચૂકી ગયેલા કૉલ"</string>
<string name="notification_missedCallTicker" msgid="6731461957487087769">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> નો કૉલ ચૂકી ગયાં"</string>
<string name="notification_missedCall_call_back" msgid="7900333283939789732">"કૉલ બેક"</string>
- <string name="notification_missedCall_message" msgid="4054698824390076431">"મેસેજ"</string>
+ <string name="notification_missedCall_message" msgid="4054698824390076431">"સંદેશ"</string>
<string name="notification_disconnectedCall_title" msgid="1790131923692416928">"ડિસ્કનેક્ટ કરેલો કૉલ"</string>
<string name="notification_disconnectedCall_body" msgid="600491714584417536">"ઇમર્જન્સી કૉલને કારણે <xliff:g id="CALLER">%s</xliff:g>નો કૉલ ડિસ્કનેક્ટ કરવામાં આવ્યો છે."</string>
<string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"ઇમર્જન્સી કૉલને કારણે તમારો કૉલ ડિસ્કનેક્ટ કરવામાં આવ્યો છે."</string>
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index e909686..918051a 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 7adae61..02c91fb 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index b9775e9..cdda34a 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 962fd89..d85d037 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index c378d37..84c0d39 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 7d3b6bb..db7dbeb 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 3f78593..ad070d6 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index d1d1c70..d557599 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 0426975..73b85d9 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 3aaa73e..33c5a47 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 2836b5d..29fdc4a 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index b00ab2b..64e47ef 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 885fc65..8109de2 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -53,7 +53,7 @@
<string name="no_vm_number" msgid="2179959110602180844">"ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯು ಕಾಣೆಯಾಗಿದೆ"</string>
<string name="no_vm_number_msg" msgid="1339245731058529388">"ಸಿಮ್ ಕಾರ್ಡ್ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
<string name="add_vm_number_str" msgid="5179510133063168998">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
- <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
+ <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಮಾಡುವುದೇ?"</string>
<string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
<string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
<string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಅಪ್ಲಿಕೇಶನ್ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index f428191..6b4c2f1 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 31ffd48..aa8ce3e 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 6d176d2..45c2b70 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index b530089..5e8b1f2 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 4219487..0433037 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 0b5e03b..4873380 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index beb730d..9e6b8ca 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 71231dc..2c90998 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index dfff80f..263433d 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 0c5b1dc..4a8d554 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7a5dbbf..3511bca 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 77331ce..fb4dc97 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index cdf6c1f..8c02676 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 3e7b071..726ab60 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 8b52919..c25ec86 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 351cf15..65073e2 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index caf5fd7..a10d29f 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a1e725e..0b279b4 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 10b51b4..a5628c4 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 8b4191e..2332d4d 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 78f5a6c..139108d 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 8ccff4a..e3faf49 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 38e7417..f7606ec 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index edc41a3..138524b 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 80d1d77..0a36a40 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 617f8d8..b846841 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 41d4d16..acc6dc6 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index e6be099..84f7294 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 3b812ea..18b5861 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index edd0d95..5ed2ebe 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index f09de86..678af2d 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index a4d5196..495c191 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index da9416a..1309682 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 551632a..7b81d25 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 146d720..689828e 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 0672e36..688b6a7 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 7481c22..0920d3b 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 2cbdd75..b926e55 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index e769d51..b422c73 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 26a8db9..21b8ae9 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 099173f..fbde58b 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -123,4 +123,11 @@
<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>
</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index b0e50b0..15f765b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -73,4 +73,8 @@
<!-- When set, Telecom will attempt to bind to the {@link CallDiagnosticService} implementation
defined by the app with this package name. -->
<string name="call_diagnostic_service_package_name"></string>
+
+ <!-- When true, the options in the call blocking settings to block unavailable and unknown
+ callers are combined into a single toggle. -->
+ <bool name="combine_options_to_block_unavailable_and_unknown_callers">true</bool>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bf5abca..d67df4b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -381,4 +381,18 @@
<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>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 53e1bcb..c8b24d3 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -29,19 +29,21 @@
<style name="Theme.Telecom.DialerSettings" parent="@android:style/Theme.DeviceDefault.Light">
<item name="android:forceDarkAllowed">true</item>
<item name="android:actionBarStyle">@style/TelecomDialerSettingsActionBarStyle</item>
- <item name="android:actionOverflowButtonStyle">@style/TelecomDialerSettingsActionOverflowButtonStyle</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
+ </style>
+
+ <style name="Theme.Telecom.EnableAccount" parent="Theme.Telecom.DialerSettings">
+ <item name="android:actionOverflowButtonStyle">
+ @style/TelecomDialerSettingsActionOverflowButtonStyle
+ </item>
<item name="android:windowContentOverlay">@null</item>
</style>
- <style name="Theme.Telecom.BlockedNumbers" parent="@android:style/Theme.DeviceDefault.Light">
- <item name="android:forceDarkAllowed">true</item>
- <item name="android:actionBarStyle">@style/TelecomDialerSettingsActionBarStyle</item>
- <item name="android:windowLightStatusBar">true</item>
- <item name="android:windowLightNavigationBar">true</item>
- <item name="android:windowContentOverlay">@null</item>
+ <style name="Theme.Telecom.BlockedNumbers" parent="Theme.Telecom.DialerSettings">
<item name="android:listDivider">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
</style>
<style name="TelecomDialerSettingsActionBarStyle" parent="android:Widget.DeviceDefault.ActionBar">
diff --git a/res/xml/activity_blocked_numbers.xml b/res/xml/activity_blocked_numbers.xml
index f884ec9..e77184d 100644
--- a/res/xml/activity_blocked_numbers.xml
+++ b/res/xml/activity_blocked_numbers.xml
@@ -70,6 +70,8 @@
android:layout_height="wrap_content"
android:text="@string/blocked_numbers_msg"
android:paddingBottom="@dimen/blocked_numbers_extra_large_padding"
+ android:clickable="false"
+ android:longClickable="false"
style="@style/BlockedNumbersTextPrimary2" />
<TextView
diff --git a/res/xml/add_blocked_number_dialog.xml b/res/xml/add_blocked_number_dialog.xml
index 35ab633..c344280 100644
--- a/res/xml/add_blocked_number_dialog.xml
+++ b/res/xml/add_blocked_number_dialog.xml
@@ -28,6 +28,8 @@
android:text="@string/add_blocked_dialog_body"
android:paddingBottom="@dimen/blocked_numbers_large_padding"
android:gravity="start"
+ android:clickable="false"
+ android:longClickable="false"
style="@style/BlockedNumbersTextPrimary2" />
<EditText
android:id="@+id/add_blocked_number"
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index 0aaeca9..bbcf858 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -16,6 +16,8 @@
package com.android.server.telecom;
+import static java.util.Map.entry;
+
import android.content.Context;
import android.os.SystemProperties;
@@ -69,103 +71,102 @@
public static final String ANALYTICS_DUMPSYS_ARG = "analytics";
private static final String CLEAR_ANALYTICS_ARG = "clear";
- public static final Map<String, Integer> sLogEventToAnalyticsEvent =
- new HashMap<String, Integer>() {{
- put(LogUtils.Events.SET_SELECT_PHONE_ACCOUNT,
- AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT);
- put(LogUtils.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD);
- put(LogUtils.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD);
- put(LogUtils.Events.SWAP, AnalyticsEvent.SWAP);
- put(LogUtils.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING);
- put(LogUtils.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH);
- put(LogUtils.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE);
- put(LogUtils.Events.SET_PARENT, AnalyticsEvent.SET_PARENT);
- put(LogUtils.Events.MUTE, AnalyticsEvent.MUTE);
- put(LogUtils.Events.UNMUTE, AnalyticsEvent.UNMUTE);
- put(LogUtils.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT);
- put(LogUtils.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE);
- put(LogUtils.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET);
- put(LogUtils.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER);
- put(LogUtils.Events.SILENCE, AnalyticsEvent.SILENCE);
- put(LogUtils.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED);
- put(LogUtils.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED);
- put(LogUtils.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED);
- put(LogUtils.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD);
- put(LogUtils.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD);
- put(LogUtils.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL);
- put(LogUtils.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT);
- put(LogUtils.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT);
- put(LogUtils.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE);
- put(LogUtils.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED);
- put(LogUtils.Events.SET_HOLD, AnalyticsEvent.SET_HOLD);
- put(LogUtils.Events.SET_DIALING, AnalyticsEvent.SET_DIALING);
- put(LogUtils.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION);
- put(LogUtils.Events.BIND_CS, AnalyticsEvent.BIND_CS);
- put(LogUtils.Events.CS_BOUND, AnalyticsEvent.CS_BOUND);
- put(LogUtils.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT);
- put(LogUtils.Events.DIRECT_TO_VM_INITIATED, AnalyticsEvent.DIRECT_TO_VM_INITIATED);
- put(LogUtils.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED);
- put(LogUtils.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED);
- put(LogUtils.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED);
- put(LogUtils.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT);
- }};
+ 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.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 =
- new HashMap<String, Integer> () {{
- put(LogUtils.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL);
- put(LogUtils.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL);
- put(LogUtils.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL);
- put(LogUtils.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL);
- put(LogUtils.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL);
- put(LogUtils.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE);
- put(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE);
- put(LogUtils.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE);
- put(LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
- SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE);
- put(LogUtils.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE);
- put(LogUtils.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING);
- put(LogUtils.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING);
- put(LogUtils.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED);
- put(LogUtils.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD);
- put(LogUtils.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL);
- put(LogUtils.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED);
- put(LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
- SessionTiming.CSW_ADD_CONFERENCE_CALL);
+ 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));
- }};
-
- public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming =
- new HashMap<String, Integer>() {{
- put(LogUtils.Events.Timings.ACCEPT_TIMING,
- ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING);
- put(LogUtils.Events.Timings.REJECT_TIMING,
- ParcelableCallAnalytics.EventTiming.REJECT_TIMING);
- put(LogUtils.Events.Timings.DISCONNECT_TIMING,
- ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING);
- put(LogUtils.Events.Timings.HOLD_TIMING,
- ParcelableCallAnalytics.EventTiming.HOLD_TIMING);
- put(LogUtils.Events.Timings.UNHOLD_TIMING,
- ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING);
- put(LogUtils.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING,
- ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING);
- put(LogUtils.Events.Timings.BIND_CS_TIMING,
- ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING);
- put(LogUtils.Events.Timings.SCREENING_COMPLETED_TIMING,
- ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING);
- put(LogUtils.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING,
- ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING);
- put(LogUtils.Events.Timings.BLOCK_CHECK_FINISHED_TIMING,
- ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING);
- put(LogUtils.Events.Timings.FILTERING_COMPLETED_TIMING,
- ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING);
- put(LogUtils.Events.Timings.FILTERING_TIMED_OUT_TIMING,
- ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING);
- put(LogUtils.Events.Timings.START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING,
- ParcelableCallAnalytics.EventTiming.
- START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING);
- }};
+ 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.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());
@@ -232,12 +233,12 @@
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
@@ -436,17 +437,17 @@
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
@@ -502,7 +503,7 @@
.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()
@@ -551,7 +552,6 @@
}
private String getMissedReasonString() {
- //TODO: Implement this
StringBuilder s = new StringBuilder();
s.append('[');
if ((missedReason & AUTO_MISSED_EMERGENCY_CALL) != 0) {
@@ -612,6 +612,7 @@
}
}
}
+
public static final String TAG = "TelecomAnalytics";
// Constants for call direction
@@ -846,13 +847,13 @@
}
@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 0000000..7c21419
--- /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 0000000..c34d211
--- /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 7f51f1b..3fbac1f 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -18,7 +18,6 @@
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.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 @@
/** 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 @@
* 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 @@
* 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 @@
}
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 60016fd..25890d9 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.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 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 @@
* 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 @@
/**
* 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 @@
@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 @@
@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 @@
*/
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;
@@ -353,6 +371,17 @@
/** 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 @@
*/
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,6 +526,8 @@
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;
@@ -529,6 +572,32 @@
*/
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
@@ -769,8 +838,11 @@
mClockProxy = clockProxy;
mToastFactory = toastFactory;
mCreationTimeMillis = mClockProxy.currentTimeMillis();
+ mCreationElapsedRealtimeMillis = mClockProxy.elapsedRealtime();
mMissedReason = MISSED_REASON_NOT_MISSED;
mStartRingTime = 0;
+
+ mCallStateChangedAtomWriter.setExistingCallCount(callsManager.getCalls().size());
}
/**
@@ -1038,10 +1110,9 @@
@Override
public ConnectionServiceFocusManager.ConnectionServiceFocus getConnectionServiceWrapper() {
- return mConnectionService;
+ return (!mIsTransactionalCall ? mConnectionService : mTransactionalService);
}
- @VisibleForTesting
public int getState() {
return mState;
}
@@ -1248,10 +1319,15 @@
}
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 +1348,34 @@
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 +1498,10 @@
}
}
+ public Uri getContactPhotoUri() {
+ return mCallerInfo != null ? mCallerInfo.getContactDisplayPhotoUri() : null;
+ }
+
public String getCallerDisplayName() {
return mCallerDisplayName;
}
@@ -1420,6 +1521,12 @@
}
}
+ void setContactPhotoUri(Uri contactPhotoUri) {
+ if (mCallerInfo != null) {
+ mCallerInfo.SetContactDisplayPhotoUri(contactPhotoUri);
+ }
+ }
+
public String getName() {
return mCallerInfo == null ? null : mCallerInfo.getName();
}
@@ -1472,12 +1579,20 @@
* @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 +1602,21 @@
}
/**
+ * @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 +1707,11 @@
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 +1719,32 @@
}
checkIfVideoCapable();
checkIfRttCapable();
+
+ if (accountHandle != null) {
+ mCallStateChangedAtomWriter.setUid(
+ accountHandle.getComponentName().getPackageName(),
+ mContext.getPackageManager());
+ }
+ }
+
+ public UserHandle getUserHandleFromTargetPhoneAccount() {
+ return mTargetPhoneAccountHandle == null
+ ? mCallsManager.getCurrentUserHandle() :
+ mTargetPhoneAccountHandle.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 +1877,36 @@
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;
}
@@ -1897,8 +2088,12 @@
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 +2188,10 @@
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 +2292,6 @@
return mConferenceLevelActiveCall;
}
- @VisibleForTesting
public ConnectionServiceWrapper getConnectionService() {
return mConnectionService;
}
@@ -2192,6 +2388,7 @@
CallIdMapper idMapper,
ParcelableConference conference) {
Log.v(this, "handleCreateConferenceSuccessful %s", conference);
+ mIsCreateConnectionComplete = true;
setTargetPhoneAccount(conference.getPhoneAccount());
setHandle(conference.getHandle(), conference.getHandlePresentation());
@@ -2201,7 +2398,7 @@
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 +2422,12 @@
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 +2436,7 @@
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 +2470,8 @@
@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 +2492,8 @@
@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 +2573,6 @@
disconnect(0);
}
- @VisibleForTesting
public void disconnect(String reason) {
disconnect(0, reason);
}
@@ -2400,7 +2601,7 @@
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 +2612,10 @@
// 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 +2684,8 @@
// {@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 +2777,10 @@
// 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 +2791,10 @@
// 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 +2815,10 @@
// 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 +2828,10 @@
} 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 +2850,9 @@
@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 +2872,9 @@
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 +2894,9 @@
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 +2916,9 @@
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 +2943,6 @@
}
}
- @VisibleForTesting
public boolean isActive() {
return mState == CallState.ACTIVE;
}
@@ -2729,18 +2953,45 @@
}
/**
+ * 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 +3001,7 @@
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 +3035,10 @@
// 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 +3073,9 @@
// 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 +3129,9 @@
}
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 +3140,9 @@
}
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 +3151,9 @@
}
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 +3163,9 @@
@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 +3176,9 @@
@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 +3204,9 @@
}
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 +3234,11 @@
* 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 +3286,7 @@
* @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 +3309,8 @@
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 +3318,7 @@
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 +3341,7 @@
));
}
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 +3350,17 @@
}
/**
+ * 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 +3656,14 @@
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 +3729,10 @@
}
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 +3742,11 @@
}
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 +3770,14 @@
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 +3798,12 @@
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 +3956,12 @@
}
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 +3986,10 @@
return mCallDirection == CALL_DIRECTION_UNKNOWN;
}
+ public boolean isOutgoing() {
+ return mCallDirection == CALL_DIRECTION_OUTGOING;
+ }
+
/**
* Determines if this call is in a disconnecting state.
*
@@ -3772,7 +4084,8 @@
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 +4116,15 @@
* @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 +4169,12 @@
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 +4490,20 @@
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 +4589,56 @@
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 0000000..045671e
--- /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 1863cde..38e6b00 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 @@
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 @@
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 @@
@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 @@
}
/**
+ * 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 @@
* @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 @@
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.getUserHandleFromTargetPhoneAccount();
+ // 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 @@
.setHasHoldingCalls(mHoldingCalls.size() > 0)
.setHasAudioProcessingCalls(mAudioProcessingCalls.size() > 0)
.setIsTonePlaying(mIsTonePlaying)
+ .setIsStreaming(mStreamingCall != null)
.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 a1c5f4b..9ad9094 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -19,12 +19,12 @@
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 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 @@
", hasAudioProcessingCalls=" + hasAudioProcessingCalls +
", isTonePlaying=" + isTonePlaying +
", foregroundCallIsVoip=" + foregroundCallIsVoip +
+ ", isStreaming=" + isStreaming +
", session=" + session +
'}';
}
@@ -84,6 +87,7 @@
private boolean mHasAudioProcessingCalls;
private boolean mIsTonePlaying;
private boolean mForegroundCallIsVoip;
+ private boolean mIsStreaming;
private Session mSession;
public Builder setHasActiveOrDialingCalls(boolean hasActiveOrDialingCalls) {
@@ -121,9 +125,15 @@
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 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 @@
// 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 @@
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 @@
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 @@
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 @@
" 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 @@
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 @@
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 @@
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 @@
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 @@
addState(mSimCallFocusState);
addState(mVoipCallFocusState);
addState(mAudioProcessingFocusState);
+ addState(mStreamingFocusState);
addState(mOtherFocusState);
setInitialState(mUnfocusedState);
start();
@@ -747,6 +858,7 @@
.setHasHoldingCalls(false)
.setIsTonePlaying(false)
.setForegroundCallIsVoip(false)
+ .setIsStreaming(false)
.setSession(Log.createSubsession())
.build());
}
@@ -800,12 +912,15 @@
// 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 2ed7d95..531f42e 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -48,6 +48,8 @@
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 @@
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 @@
/** 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 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 @@
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 @@
mCallAudioManager.notifyAudioOperationsComplete();
}
return HANDLED;
+ case STREAMING_FORCE_ENABLED:
+ transitionTo(mStreamingState);
+ return HANDLED;
default:
return NOT_HANDLED;
}
@@ -1105,6 +1122,9 @@
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 @@
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 @@
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 @@
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl) {
+ int earpieceControl,
+ Executor asyncTaskExecutor) {
super(NAME);
mContext = context;
mCallsManager = callsManager;
@@ -1447,7 +1533,7 @@
mStatusBarNotifier = statusBarNotifier;
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
-
+ mAsyncTaskExecutor = asyncTaskExecutor;
createStates(earpieceControl);
}
@@ -1459,7 +1545,7 @@
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 @@
mStatusBarNotifier = statusBarNotifier;
mAudioServiceFactory = audioServiceFactory;
mLock = callsManager.getLock();
+ mAsyncTaskExecutor = asyncTaskExecutor;
createStates(earpieceControl);
}
@@ -1494,6 +1581,7 @@
addState(mQuiescentHeadsetRoute);
addState(mQuiescentBluetoothRoute);
addState(mQuiescentSpeakerRoute);
+ addState(mStreamingState);
mStateNameToRouteCode = new HashMap<>(8);
@@ -1506,12 +1594,14 @@
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) {
@@ -1547,7 +1637,8 @@
new IntentFilter(AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED));
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,26 +1731,32 @@
private void setSpeakerphoneOn(boolean on) {
Log.i(this, "turning speaker phone %s", on);
- AudioDeviceInfo speakerDevice = null;
- for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
- if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- speakerDevice = info;
- break;
+ final boolean hasAnyCalls = mCallsManager.hasAnyCalls();
+ // These APIs are all via two-way binder calls so can potentially block Telecom. Since none
+ // of this has to happen in the Telecom lock we'll offload it to the async executor.
+ mAsyncTaskExecutor.execute(() -> {
+ AudioDeviceInfo speakerDevice = null;
+ for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
+ if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ speakerDevice = info;
+ break;
+ }
}
- }
- boolean speakerOn = false;
- if (speakerDevice != null && on) {
- boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
- if (result) {
- speakerOn = true;
+ boolean speakerOn = false;
+ if (speakerDevice != null && on) {
+ boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
+ if (result) {
+ speakerOn = true;
+ }
+ } else {
+ AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
+ if (curDevice != null
+ && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ mAudioManager.clearCommunicationDevice();
+ }
}
- } else {
- AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
- if (curDevice != null && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- mAudioManager.clearCommunicationDevice();
- }
- }
- mStatusBarNotifier.notifySpeakerphone(speakerOn);
+ mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && speakerOn);
+ });
}
private void setBluetoothOn(String address) {
@@ -1764,16 +1861,18 @@
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 7bd7288..6c7ee38 100644
--- a/src/com/android/server/telecom/CallDiagnosticServiceController.java
+++ b/src/com/android/server/telecom/CallDiagnosticServiceController.java
@@ -25,7 +25,6 @@
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 @@
* 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 0000000..82164b3
--- /dev/null
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -0,0 +1,398 @@
+/*
+ * 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) {
+ endpoint = new CallEndpoint(
+ device.getName() != null ? device.getName() : "",
+ 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 0000000..a9b03c3
--- /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 7f864b8..7953324 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -182,9 +182,10 @@
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
old mode 100755
new mode 100644
index 0ec2362..a7bbf84
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -386,7 +386,12 @@
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 0168590..9426100 100644
--- a/src/com/android/server/telecom/CallScreeningServiceHelper.java
+++ b/src/com/android/server/telecom/CallScreeningServiceHelper.java
@@ -229,8 +229,9 @@
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 0411ecc..c35c7ec 100644
--- a/src/com/android/server/telecom/CallState.java
+++ b/src/com/android/server/telecom/CallState.java
@@ -132,6 +132,46 @@
*/
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 0000000..6276a7d
--- /dev/null
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -0,0 +1,394 @@
+/*
+ * 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.VoipCallTransaction;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
+
+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) {
+ 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.d(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.getInitiatingUser();
+ mContext = context;
+ }
+
+ @SuppressLint("LongLogTag")
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(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;
+ }
+
+ 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.d(this, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ resetController();
+ future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ null));
+ return future;
+ }
+ }
+
+ @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);
+ case CallState.DISCONNECTING:
+ case CallState.DISCONNECTED:
+ Log.addEvent(call, LogUtils.Events.STOP_STREAMING);
+ transaction = new CallStreamingStateChangeTransaction(
+ StreamingCall.STATE_DISCONNECTED);
+ 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() {
+ try {
+ if (mService != null) {
+ mService.onCallStreamingStopped();
+ }
+ } catch (RemoteException e) {
+ Log.e(this, e, "Exception when stop call streaming");
+ }
+ 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
old mode 100755
new mode 100644
index 3fce799..d457cc8
--- 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,13 @@
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.app.ActivityManager;
import android.app.AlertDialog;
import android.app.KeyguardManager;
+import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -50,6 +52,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ResolveInfoFlags;
import android.content.pm.UserInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
@@ -58,13 +61,14 @@
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;
@@ -73,9 +77,13 @@
import android.provider.BlockedNumberContract;
import android.provider.BlockedNumberContract.SystemContract;
import android.provider.CallLog.Calls;
+import android.provider.DeviceConfig;
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 +101,9 @@
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;
@@ -108,22 +118,26 @@
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.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 +152,12 @@
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 +171,20 @@
* 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 +194,9 @@
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 +205,7 @@
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 +253,45 @@
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};
@@ -374,6 +439,15 @@
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 ConnectionServiceFocusManager.CallsManagerRequester mRequester =
new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -394,14 +468,18 @@
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 +510,14 @@
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 +552,15 @@
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) {
+
mContext = context;
mLock = lock;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
@@ -511,6 +577,7 @@
mTimeoutsAdapter = timeoutsAdapter;
mEmergencyCallHelper = emergencyCallHelper;
mCallerInfoLookupHelper = callerInfoLookupHelper;
+ mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
mDtmfLocalTonePlayer =
new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
@@ -522,7 +589,8 @@
wiredHeadsetManager,
statusBarNotifier,
audioServiceFactory,
- CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT
+ CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+ asyncTaskExecutor
);
callAudioRouteStateMachine.initialize();
@@ -532,7 +600,6 @@
bluetoothManager,
wiredHeadsetManager,
mDockManager);
-
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
InCallTonePlayer.MediaPlayerFactory mediaPlayerFactory =
(resourceId, attributes) ->
@@ -549,11 +616,14 @@
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 +644,16 @@
mClockProxy = clockProxy;
mToastFactory = toastFactory;
mRoleManagerAdapter = roleManagerAdapter;
+ mTransactionManager = transactionManager;
+ mBlockedNumbersAdapter = blockedNumbersAdapter;
+ mCallStreamingController = new CallStreamingController(mContext, mLock);
+ mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
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 +662,15 @@
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);
+
+ mVoipCallMonitor.startMonitor();
// There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
final UserManager userManager = UserManager.get(mContext);
@@ -603,6 +684,10 @@
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 +723,11 @@
}
@Override
+ @VisibleForTesting
public void onSuccessfulOutgoingCall(Call call, int callState) {
Log.v(this, "onSuccessfulOutgoingCall, %s", call);
- call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp());
+ call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp(
+ call.getUserHandleFromTargetPhoneAccount()));
setCallState(call, callState, "successful outgoing call");
if (!mCalls.contains(call)) {
@@ -659,8 +746,7 @@
@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 +759,19 @@
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 +791,11 @@
private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) {
incomingCall.setIsUsingCallFiltering(true);
String carrierPackageName = getCarrierPackageName();
- String defaultDialerPackageName = TelecomManager.from(mContext).getDefaultDialerPackage();
- String userChosenPackageName = getRoleManagerAdapter().getDefaultCallScreeningApp();
+ UserHandle userHandle = incomingCall.getUserHandleFromTargetPhoneAccount();
+ 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 +806,7 @@
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 +824,7 @@
mContext, this, appLabelProxy, converter);
}
graph.addFilter(voicemailFilter);
+ graph.addFilter(dndCallFilter);
graph.addFilter(blockCheckerFilter);
graph.addFilter(carrierCallScreeningServiceFilter);
graph.addFilter(callScreeningServiceFilter);
@@ -774,6 +872,9 @@
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 +892,7 @@
}
// 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 +912,9 @@
if (result.shouldAllowCall) {
incomingCall.setPostCallPackageName(
- getRoleManagerAdapter().getDefaultCallScreeningApp());
+ getRoleManagerAdapter().getDefaultCallScreeningApp(
+ incomingCall.getUserHandleFromTargetPhoneAccount()
+ ));
Log.i(this, "onCallFilteringComplete: allow call.");
if (hasMaximumManagedRingingCalls(incomingCall)) {
@@ -861,7 +963,8 @@
}
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 +1004,15 @@
@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 +1232,6 @@
}
}
- @VisibleForTesting
public Call getForegroundCall() {
if (mCallAudioManager == null) {
// Happens when getForegroundCall is called before full initialization.
@@ -1137,6 +1240,15 @@
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 +1284,18 @@
return mInCallController;
}
+ public CallEndpointController getCallEndpointController() {
+ return mCallEndpointController;
+ }
+
EmergencyCallHelper getEmergencyCallHelper() {
return mEmergencyCallHelper;
}
+ EmergencyCallDiagnosticLogger getEmergencyCallDiagnosticLogger() {
+ return mEmergencyCallDiagnosticLogger;
+ }
+
public DefaultDialerCache getDefaultDialerCache() {
return mDefaultDialerCache;
}
@@ -1239,6 +1359,11 @@
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 +1380,7 @@
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);
@@ -1265,7 +1390,7 @@
handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
Call call = new Call(
- getNextCallId(),
+ generateNextCallId(extras),
mContext,
this,
mLock,
@@ -1281,7 +1406,17 @@
mClockProxy,
mToastFactory);
- // Ensure new calls related to self-managed calls/connections are set as such. This will
+ // 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);
+ }
+
+ // 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.
@@ -1298,7 +1433,7 @@
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 +1445,7 @@
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 +1535,35 @@
}
}
- 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 (mUserManager.isQuietModeEnabled(call.getUserHandleFromTargetPhoneAccount())
+ && !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 +1572,7 @@
// 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 +1580,18 @@
} 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) {
@@ -1455,6 +1619,7 @@
setIntentExtrasAndStartTime(call, extras);
call.addListener(this);
+ notifyStartCreateConnection(call);
call.startCreateConnection(mPhoneAccountRegistrar);
}
@@ -1518,6 +1683,14 @@
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,
@@ -1544,7 +1717,7 @@
// 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,
@@ -1559,8 +1732,35 @@
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);
+ call.setTargetPhoneAccount(requestedAccountHandle);
+ }
+
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
@@ -1631,6 +1831,18 @@
// 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 +1882,7 @@
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 +1915,7 @@
notifyCreateConnectionFailed(
finalCall.getTargetPhoneAccount(), finalCall);
}
+ finalCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
return CompletableFuture.completedFuture(null);
}
@@ -1761,9 +1974,36 @@
return CompletableFuture.completedFuture(null);
}
if (accountSuggestions == null || accountSuggestions.isEmpty()) {
+ if (isSwitchToManagedProfileDialogFlagEnabled()) {
+ 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 +2050,8 @@
dialerSelectPhoneAccountFuture.thenAcceptBothAsync(contactLookupFuture,
(callPhoneAccountHandlePair, uriCallerInfoPair) -> {
Call theCall = callPhoneAccountHandlePair.first;
+ UserHandle userHandleForCallScreening = theCall.
+ getUserHandleFromTargetPhoneAccount();
boolean isInContacts = uriCallerInfoPair.second != null
&& uriCallerInfoPair.second.contactExists;
Log.d(CallsManager.this, "outgoingCallIdStage: isInContacts=%s",
@@ -1820,10 +2062,12 @@
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 +2125,7 @@
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 +2139,46 @@
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 isSwitchToManagedProfileDialogFlagEnabled() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
+ "enable_switch_to_managed_profile_dialog", false);
+ }
+
+ private boolean showSwitchToManagedProfileDialog(Uri callUri, UserHandle initiatingUser,
+ int managedProfileUserId) {
+ try {
+ 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).size() != 0) {
+ mContext.startActivityAsUser(showErrorIntent, initiatingUser);
+ return true;
+ }
+ } catch (Exception e) {
+ Log.w(this, "Failed to launch switch to managed profile dialog");
+ }
+ return false;
+ }
+
public void startConference(List<Uri> participants, Bundle clientExtras, String callingPackage,
UserHandle initiatingUser) {
@@ -1934,7 +2218,8 @@
private void bindForOutgoingCallerId(Call theCall) {
// Find the user chosen call screening app.
String callScreeningApp =
- mRoleManagerAdapter.getDefaultCallScreeningApp();
+ mRoleManagerAdapter.getDefaultCallScreeningApp(
+ theCall.getUserHandleFromTargetPhoneAccount());
CompletableFuture future =
new CallScreeningServiceHelper(mContext,
@@ -2090,27 +2375,31 @@
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.getUserHandleFromTargetPhoneAccount();
// 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 +2427,7 @@
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;
@@ -2420,12 +2709,26 @@
// 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.getInitiatingUser(), false).isEmpty()) {
// If there are no call capable accounts, disconnect the call.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.CANCELED,
"No registered PhoneAccounts"));
@@ -2456,6 +2759,10 @@
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 +3156,7 @@
}
@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 +3184,18 @@
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 +3210,14 @@
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 +3256,14 @@
}
}
+ @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 +3303,14 @@
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();
@@ -3034,6 +3369,30 @@
}
}
+ @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 +3412,8 @@
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 +3430,11 @@
*/
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 +3460,7 @@
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 +3486,45 @@
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 +3559,6 @@
}
}
- @VisibleForTesting
public void markCallAsOnHold(Call call) {
setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
}
@@ -3169,8 +3569,9 @@
*
* @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 +3596,17 @@
}
}
+ // 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 +3651,7 @@
/**
* 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 +3676,57 @@
* @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) {
+ 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);
+ }
+ }
- // 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)");
+ /**
+ * 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 +3796,6 @@
return false;
}
- boolean hasActiveOrHoldingCall() {
- return getFirstCallWithState(CallState.ACTIVE, CallState.ON_HOLD) != null;
- }
-
boolean hasRingingCall() {
return getFirstCallWithState(CallState.RINGING, CallState.ANSWERED) != null;
}
@@ -3490,15 +3917,6 @@
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 +4040,11 @@
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 +4054,7 @@
call.setVideoState(parcelableConference.getVideoState());
call.setVideoProvider(parcelableConference.getVideoProvider());
call.setStatusHints(parcelableConference.getStatusHints());
- call.putExtras(Call.SOURCE_CONNECTION_SERVICE, parcelableConference.getExtras());
+ call.putConnectionServiceExtras(parcelableConference.getExtras());
// 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,6 +4139,10 @@
*/
@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);
@@ -3748,6 +4175,11 @@
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();
@@ -4008,37 +4440,6 @@
}
}
- 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.
@@ -4101,6 +4502,60 @@
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);
@@ -4195,23 +4650,29 @@
/**
* 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 +4753,7 @@
}
// 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 +4764,12 @@
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 +4841,14 @@
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 +4870,20 @@
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 +4899,7 @@
+ " of new outgoing call.");
return true;
}
+ call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
return false;
}
@@ -4437,12 +4919,25 @@
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 +4961,7 @@
}
// 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 +5005,47 @@
}
}
+ /**
+ * 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();
+ }
+ });
}
/**
@@ -4567,7 +5093,7 @@
call.setCallerDisplayName(connection.getCallerDisplayName(),
connection.getCallerDisplayNamePresentation());
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 +5112,9 @@
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 +5219,42 @@
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);
- } else {
- return !hasMaximumSelfManagedRingingCalls(excludeCall, phoneAccountHandle) &&
- !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle);
+ 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 {
+ 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 +5426,7 @@
*
* @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 +5482,20 @@
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 +5517,13 @@
impl.dump(pw);
pw.decreaseIndent();
}
+
+ if (mConnectionSvrFocusMgr != null) {
+ pw.println("mConnectionSvrFocusMgr:");
+ pw.increaseIndent();
+ mConnectionSvrFocusMgr.dump(pw);
+ pw.decreaseIndent();
+ }
}
/**
@@ -4963,11 +5532,14 @@
* @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 +5611,9 @@
} else {
call.setConnectionService(service);
service.createConnectionFailed(call);
+ if (!mCalls.contains(call)){
+ mListeners.forEach(l -> l.onCreateConnectionFailed(call));
+ }
}
}
@@ -5061,9 +5636,20 @@
} 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
@@ -5227,7 +5813,7 @@
// Disconnect all self-managed calls to make priority for emergency call.
disconnectSelfManagedCalls("emergency call");
}
-
+ notifyStartCreateConnection(call);
call.startCreateConnection(mPhoneAccountRegistrar);
}
@@ -5404,7 +5990,7 @@
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 +5998,10 @@
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 +6023,12 @@
@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 +6109,82 @@
}
}
+ /**
+ * 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 +6204,8 @@
* 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 +6254,19 @@
}
/**
+ * 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.getUserHandleFromTargetPhoneAccount().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 +6347,22 @@
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;
+ }
}
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 55c7b53..43f3b90 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -18,12 +18,14 @@
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 @@
}
@Override
+ public void onCreateConnectionFailed(Call call) {
+ }
+
+ @Override
public void onCallStateChanged(Call call, int oldState, int newState) {
}
@@ -57,6 +63,18 @@
}
@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 @@
}
@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 737ce5a..ae8febf 100644
--- a/src/com/android/server/telecom/CarModeTracker.java
+++ b/src/com/android/server/telecom/CarModeTracker.java
@@ -150,7 +150,9 @@
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 aa0a64f..3694727 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -25,13 +25,16 @@
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 @@
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 @@
* @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 @@
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 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 @@
&& 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 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
old mode 100755
new mode 100644
index 5bb1dbe..5b727ab
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -23,15 +23,21 @@
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.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 +48,15 @@
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 +71,11 @@
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 +89,9 @@
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 {
@@ -358,9 +375,8 @@
if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
mCallsManager.markCallAsDisconnected(
call, new DisconnectCause(DisconnectCause.REMOTE));
- } else {
- mCallsManager.markCallAsRemoved(call);
}
+ mCallsManager.markCallAsRemoved(call);
}
}
} catch (Throwable t) {
@@ -435,7 +451,13 @@
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);
@@ -724,6 +746,26 @@
}
@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);
@@ -754,7 +796,7 @@
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 +919,9 @@
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 +933,7 @@
// 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)) {
@@ -1196,6 +1241,71 @@
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 +1332,8 @@
* @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 +1393,141 @@
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 +1603,7 @@
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 +1920,54 @@
}
}
+ /** @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 3561211..6702f03 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 @@
// 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.getUserHandleFromTargetPhoneAccount();
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 @@
// 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 a4a0242..3ce394e 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -265,7 +265,7 @@
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/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
new file mode 100644
index 0000000..af79da3
--- /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 5de4e5a..fbb666d 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -21,6 +21,8 @@
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 @@
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 @@
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,37 @@
return mLastEmergencyCallTimestampMillis;
}
- private void recordEmergencyCallTime() {
- mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+ void setLastOutgoingEmergencyCallPAH(PhoneAccountHandle accountHandle) {
+ mLastOutgoingEmergencyCallPAH = accountHandle;
}
- private boolean isInEmergencyCallbackWindow() {
- return System.currentTimeMillis() - getLastEmergencyCallTimeMillis()
+ 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 +122,8 @@
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 +133,65 @@
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);
- }
- if (!mHadBackgroundLocation) {
- mContext.getPackageManager().revokeRuntimePermission(systemDialerPackage,
- Manifest.permission.ACCESS_BACKGROUND_LOCATION, userHandle);
+ Manifest.permission.ACCESS_FINE_LOCATION, userHandle);
}
} catch (Exception e) {
Log.e(this, e, "Failed to revoke location permission from " + systemDialerPackage
- + ", user: " + userHandle);
+ + ", user: " + userHandle);
}
+
+ try {
+ if (!mHadBackgroundLocation && mBackgroundLocationGranted) {
+ mContext.getPackageManager().revokeRuntimePermission(systemDialerPackage,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION, userHandle);
+ }
+ } catch (Exception e) {
+ Log.e(this, e, "Failed to revoke location permission from " + systemDialerPackage
+ + ", user: " + userHandle);
+ }
+
clearPermissionGrant();
}
@@ -157,8 +209,14 @@
== 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 +227,7 @@
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 b1471c2..8e9caff 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -23,11 +23,16 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
import android.telecom.Log;
+import android.util.ArraySet;
import android.view.KeyEvent;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.Set;
+
/**
* Static class to handle listening to the headset media buttons.
*/
@@ -67,6 +72,11 @@
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 +94,7 @@
*/
public interface MediaSessionAdapter {
void setActive(boolean active);
+ void setCallback(MediaSession.Callback callback);
boolean isActive();
}
@@ -143,8 +154,10 @@
private final Context mContext;
private final CallsManager mCallsManager;
private final TelecomSystem.SyncRoot mLock;
+ private final Set<Call> mCalls = new ArraySet<>();
private MediaSessionAdapter mSession;
private KeyEvent mLastHookEvent;
+ private @CallEndpoint.EndpointType int mCurrentEndpointType;
/**
* Constructor used for testing purposes to initialize a {@link HeadsetMediaButton} with a
@@ -165,6 +178,8 @@
mCallsManager = callsManager;
mLock = lock;
mSession = adapter;
+
+ adapter.setCallback(mSessionCallback);
}
/**
@@ -204,7 +219,7 @@
return mCallsManager.onMediaButton(LONG_PRESS);
} else if (event.getAction() == KeyEvent.ACTION_UP) {
// We should not judge SHORT_PRESS by ACTION_UP event repeatCount, because it always
- // return 0.
+ // returns 0.
// Actually ACTION_DOWN event repeatCount only increases when LONG_PRESS performed.
if (mLastHookEvent != null && mLastHookEvent.getRepeatCount() == 0) {
return mCallsManager.onMediaButton(SHORT_PRESS);
@@ -218,52 +233,72 @@
return true;
}
+ @Override
+ public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+ mCurrentEndpointType = callEndpoint.getEndpointType();
+ Log.i(this, "onCallEndpointChanged: endPoint=%s", callEndpoint);
+ maybeChangeSessionState();
+ }
+
/** ${inheritDoc} */
@Override
public void onCallAdded(Call call) {
- if (call.isExternalCall()) {
- return;
- }
- handleCallAddition();
+ handleCallAddition(call);
}
/**
* Triggers session activation due to call addition.
*/
- private void handleCallAddition() {
- mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget();
- }
-
- /** ${inheritDoc} */
- @Override
- public void onCallRemoved(Call call) {
- if (call.isExternalCall()) {
- return;
- }
- handleCallRemoval();
+ private void handleCallAddition(Call call) {
+ mCalls.add(call);
+ maybeChangeSessionState();
}
/**
- * Triggers session deactivation due to call removal.
+ * Based on whether there are tracked calls and the audio is routed to a wired headset,
+ * potentially activate or deactive the media session.
*/
- private void handleCallRemoval() {
- if (!mCallsManager.hasAnyCalls()) {
+ private void maybeChangeSessionState() {
+ boolean hasNonExternalCalls = !mCalls.isEmpty()
+ && mCalls.stream().anyMatch(c -> !c.isExternalCall());
+ if (hasNonExternalCalls && mCurrentEndpointType == CallEndpoint.TYPE_WIRED_HEADSET) {
+ Log.i(this, "maybeChangeSessionState: hasCalls=%b, currentEndpointType=%s, ACTIVATE",
+ hasNonExternalCalls, CallEndpoint.endpointTypeToString(mCurrentEndpointType));
+ mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget();
+ } else {
+ Log.i(this, "maybeChangeSessionState: hasCalls=%b, currentEndpointType=%s, DEACTIVATE",
+ hasNonExternalCalls, CallEndpoint.endpointTypeToString(mCurrentEndpointType));
mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget();
}
}
/** ${inheritDoc} */
@Override
- public void onExternalCallChanged(Call call, boolean isExternalCall) {
- // Note: We don't use the onCallAdded/onCallRemoved methods here since they do checks to see
- // if the call is external or not and would skip the session activation/deactivation.
- if (isExternalCall) {
- handleCallRemoval();
- } else {
- handleCallAddition();
+ public void onCallRemoved(Call call) {
+ handleCallRemoval(call);
+ }
+
+ /**
+ * Triggers session deactivation due to call removal.
+ */
+ private void handleCallRemoval(Call call) {
+ // If we were tracking the call, potentially change session state.
+ if (mCalls.remove(call)) {
+ if (mCalls.isEmpty()) {
+ // When there are no calls, don't cache that we previously had a wired headset
+ // connected; we'll be updated on the next call.
+ mCurrentEndpointType = CallEndpoint.TYPE_UNKNOWN;
+ }
+ maybeChangeSessionState();
}
}
+ /** ${inheritDoc} */
+ @Override
+ public void onExternalCallChanged(Call call, boolean isExternalCall) {
+ maybeChangeSessionState();
+ }
+
@VisibleForTesting
/**
* @return the handler this class instance uses for operation; used for unit testing.
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 0fda5f8..9ce10bd 100755
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -19,6 +19,8 @@
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 @@
}
@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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 9a53575..3d3e3b4 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -23,11 +23,8 @@
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.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;
@@ -52,6 +48,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
import android.telecom.ConnectionService;
import android.telecom.InCallService;
import android.telecom.Log;
@@ -78,6 +75,7 @@
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 +89,21 @@
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
@@ -285,6 +298,8 @@
@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 +309,7 @@
// Notify this new added call
sendCallToService(call, mInCallServiceInfo,
- mInCallServices.get(mInCallServiceInfo));
+ mInCallServices.get(userFromCall).get(mInCallServiceInfo));
}
return CONNECTION_SUCCEEDED;
}
@@ -320,11 +335,18 @@
Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
mIsConnected = true;
mInCallServiceInfo.setBindingStartTime(mClockProxy.elapsedRealtime());
+ UserHandle userToBind = getUserFromCall(call);
+ boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userToBind);
+ // 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).
+ userToBind = isManagedProfile ? userToBind : UserHandle.CURRENT;
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 +367,7 @@
@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 +380,7 @@
// 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 +391,7 @@
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 +417,8 @@
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 +429,25 @@
}
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 +500,9 @@
// 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 +510,7 @@
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 +650,13 @@
*
* @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)
@@ -770,6 +808,9 @@
}
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 +828,7 @@
@Override
public void onConnectionPropertiesChanged(Call call, boolean didRttChange) {
- updateCall(call, false /* includeVideoProvider */, didRttChange);
+ updateCall(call, false /* includeVideoProvider */, didRttChange, null);
}
@Override
@@ -797,7 +838,7 @@
@Override
public void onVideoCallProviderChanged(Call call) {
- updateCall(call, true /* videoProviderChanged */, false);
+ updateCall(call, true /* videoProviderChanged */, false, null);
}
@Override
@@ -805,25 +846,35 @@
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 +945,7 @@
@Override
public void onRttInitiationFailure(Call call, int reason) {
notifyRttInitiationFailure(call, reason);
- updateCall(call, false, true);
+ updateCall(call, false, true, null);
}
@Override
@@ -917,6 +968,8 @@
if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
synchronized (mLock) {
String changedPackage = intent.getData().getSchemeSpecificPart();
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+ UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
List<InCallServiceBindingConnection> componentsToBind =
Arrays.stream(intent.getStringArrayExtra(
Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST))
@@ -925,13 +978,14 @@
className))
.filter(mKnownNonUiInCallServices::contains)
.flatMap(componentName -> getInCallServiceComponents(
- componentName,
+ userHandle, componentName,
IN_CALL_SERVICE_TYPE_NON_UI).stream())
.map(InCallServiceBindingConnection::new)
.collect(Collectors.toList());
- if (mNonUIInCallServiceConnections != null) {
- mNonUIInCallServiceConnections.addConnections(componentsToBind);
+ if (mNonUIInCallServiceConnections.containsKey(userHandle)) {
+ mNonUIInCallServiceConnections.get(userHandle).
+ addConnections(componentsToBind);
}
// If the current car mode app become enabled from disabled, update
@@ -988,7 +1042,8 @@
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 +1057,10 @@
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();
@@ -1136,59 +1193,71 @@
@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;
+ }
+
+ // Only send the RTT call if it's a UI in-call service
+ boolean includeRttCall = false;
+ if (inCallServiceConnection != null) {
+ includeRttCall = info.equals(inCallServiceConnection.getInfo());
+ }
+
+ componentsUpdated.add(info.getComponentName());
+ IInCallService inCallService = entry.getValue();
+
+ ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
+ true /* includeVideoProvider */,
+ mCallsManager.getPhoneAccountRegistrar(),
+ info.isExternalCallsSupported(), includeRttCall,
+ info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
+ info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
+ try {
+ inCallService.addCall(
+ sanitizeParcelableCallForService(info, parcelableCall));
+ updateCallTracking(call, info, true /* isAdd */);
+ } catch (RemoteException ignored) {
+ }
}
-
- 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());
- }
-
- componentsUpdated.add(info.getComponentName());
- IInCallService inCallService = entry.getValue();
-
- ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
- true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
- info.isExternalCallsSupported(), includeRttCall,
- info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
- info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
- try {
- inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
- updateCallTracking(call, info, true /* isAdd */);
- } catch (RemoteException ignored) {
- }
+ Log.i(this, "Call added to components: %s", componentsUpdated);
}
- Log.i(this, "Call added to components: %s", componentsUpdated);
}
}
@@ -1204,7 +1273,7 @@
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 +1296,12 @@
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 +1319,8 @@
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 +1339,38 @@
// 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();
+
+ 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) {
+ }
}
-
- 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
- );
-
- 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 +1396,63 @@
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 +1460,22 @@
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 +1535,7 @@
}
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 +1550,10 @@
}
}
- 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 +1564,28 @@
}
}
- 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;
+ }
+
+ 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 +1599,11 @@
}
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 +1617,11 @@
}
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 +1635,9 @@
}
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 +1647,9 @@
}
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 +1661,22 @@
/**
* 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 +1688,10 @@
*/
@VisibleForTesting
public void bindToServices(Call call) {
- if (mInCallServiceConnection == null) {
+ UserHandle userFromCall = getUserFromCall(call);
+ 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 +1700,30 @@
}
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);
if (carModeComponentInfo != null &&
!carModeComponentInfo.getComponentName().equals(
mDefaultDialerCache.getSystemDialerComponent())) {
carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
}
- 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,
@@ -1594,9 +1742,10 @@
mContext.registerReceiver(mPackageChangedReceiver, packageChangedFilter);
}
- private void updateNonUiInCallServices() {
+ private void updateNonUiInCallServices(Call call) {
+ UserHandle userFromCall = getUserFromCall(call);
List<InCallServiceInfo> nonUIInCallComponents =
- getInCallServiceComponents(IN_CALL_SERVICE_TYPE_NON_UI);
+ getInCallServiceComponents(userFromCall, IN_CALL_SERVICE_TYPE_NON_UI);
List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
for (InCallServiceInfo serviceInfo : nonUIInCallComponents) {
nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo));
@@ -1605,27 +1754,28 @@
.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 +1785,10 @@
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,13 +1800,16 @@
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 {
@@ -1667,38 +1820,41 @@
}
}
- 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 List<InCallServiceInfo> getInCallServiceComponents(String packageName,
- ComponentName componentName, int requestedType, boolean ignoreDisabled) {
+ 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 +1864,15 @@
if (componentName != null) {
serviceIntent.setComponent(componentName);
}
-
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 +1883,8 @@
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);
ComponentName foundComponentName =
new ComponentName(serviceInfo.packageName, serviceInfo.name);
if (requestedType == IN_CALL_SERVICE_TYPE_NON_UI) {
@@ -1780,8 +1935,8 @@
/**
* 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 +1988,7 @@
// 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 +2007,11 @@
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 +2024,7 @@
* @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 +2033,9 @@
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 +2045,8 @@
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 +2082,11 @@
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 +2112,16 @@
*
* @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 +2130,41 @@
* @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 +2179,10 @@
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 +2200,8 @@
* 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 +2220,11 @@
/**
* @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 +2243,17 @@
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 +2263,25 @@
/**
* @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 +2291,7 @@
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 +2306,7 @@
.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 +2338,32 @@
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 +2385,8 @@
// 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,16 +2435,19 @@
public void updateCarModeForConnections() {
Log.i(this, "updateCarModeForConnections: car mode apps: %s",
mCarModeTracker.getCarModeApps().stream().collect(Collectors.joining(", ")));
- if (mInCallServiceConnection != null) {
+
+ if (mInCallServiceConnections.containsKey(mCallsManager.getCurrentUserHandle())) {
+ CarSwappingInCallServiceConnection inCallServiceConnection = mInCallServiceConnections.
+ get(mCallsManager.getCurrentUserHandle());
if (shouldUseCarModeUI()) {
Log.i(this, "updateCarModeForConnections: potentially update car mode app.");
- mInCallServiceConnection.changeCarModeApp(
- mCarModeTracker.getCurrentCarModePackage());
+ inCallServiceConnection.changeCarModeApp(mCarModeTracker.getCurrentCarModePackage(),
+ mCallsManager.getCurrentUserHandle());
} else {
- if (mInCallServiceConnection.isCarMode()) {
+ if (inCallServiceConnection.isCarMode()) {
Log.i(this, "updateCarModeForConnections: car mode no longer "
+ "applicable; disabling");
- mInCallServiceConnection.disableCarMode();
+ inCallServiceConnection.disableCarMode();
}
}
}
@@ -2303,6 +2510,7 @@
&& !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 +2556,7 @@
== 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 +2584,8 @@
.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 +2594,21 @@
|| 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.getUserHandleFromTargetPhoneAccount();
+ 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)) {
+ return mCallsManager.getCurrentUserHandle();
+ }
+ return userFromCall;
+ }
+ }
}
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index 4665ec2..3cc4aac 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -170,7 +170,7 @@
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 @@
@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;
@@ -524,7 +518,7 @@
mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) {
@Override
public void loggedRun() {
- int newToneCount = sTonesPlaying.updateAndGet( t -> Math.min(0, t--));
+ int newToneCount = sTonesPlaying.updateAndGet( t -> Math.max(0, --t));
if (newToneCount == 0) {
Log.i(InCallTonePlayer.this,
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 12e780f..4e7546f 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -88,6 +88,7 @@
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 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 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,14 @@
"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 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 +240,7 @@
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 +262,8 @@
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 0000000..11f6d59
--- /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 4569950..8426d1f 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 @@
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 @@
@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 @@
mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
mLock = mCallsManager.getLock();
mDefaultDialerCache = defaultDialerCache;
+ mMmiUtils = mmiUtils;
}
/**
@@ -139,7 +139,7 @@
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 @@
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 @@
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,9 +382,9 @@
* broadcasting.
*/
callRedirectionWithService = callRedirectionProcessor
- .canMakeCallRedirectionWithService();
+ .canMakeCallRedirectionWithServiceAsUser(mCall.getInitiatingUser());
if (callRedirectionWithService) {
- callRedirectionProcessor.performCallRedirection();
+ callRedirectionProcessor.performCallRedirection(mCall.getInitiatingUser());
}
}
@@ -517,23 +527,18 @@
* 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 @@
* 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 0becaef..673b99a 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -196,6 +196,8 @@
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 @@
.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 576a289..f5a3450 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -32,6 +32,7 @@
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.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 @@
// 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.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 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";
@@ -248,7 +249,7 @@
}
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 +324,7 @@
}
// 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 +470,7 @@
// 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();
@@ -725,16 +726,13 @@
*
* @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 +746,11 @@
* @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 +767,11 @@
*/
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 +789,7 @@
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 +798,7 @@
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 +813,17 @@
*/
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 +864,190 @@
* 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.");
+ 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.");
}
- //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()
+ 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 +1065,15 @@
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 +1354,11 @@
// 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 +1376,7 @@
Log.w(this, "phoneAccount %s not found", phoneAccountHandle.getComponentName());
return false;
}
+
for (ResolveInfo resolveInfo : resolveInfos) {
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
if (serviceInfo == null) {
@@ -1220,6 +1395,15 @@
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 +1450,10 @@
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 +1466,13 @@
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 +1483,10 @@
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 +1506,8 @@
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 +1530,10 @@
}
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 +1542,7 @@
// 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 +1969,20 @@
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 23ccc1c..8507703 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -31,11 +31,13 @@
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import android.telephony.SubscriptionManager;
+import android.text.BidiFormatter;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.widget.Toast;
+import java.text.Bidi;
import java.util.ArrayList;
import java.util.List;
@@ -158,7 +160,9 @@
final String formatString = res.getString(success
? R.string.respond_via_sms_confirmation_format
: R.string.respond_via_sms_failure_format);
- final String confirmationMsg = String.format(formatString, phoneNumber);
+ final BidiFormatter phoneNumberFormatter = BidiFormatter.getInstance();
+ final String confirmationMsg = String.format(formatString,
+ phoneNumberFormatter.unicodeWrap(phoneNumber));
int startingPosition = confirmationMsg.indexOf(phoneNumber);
int endingPosition = startingPosition + phoneNumber.length();
@@ -207,10 +211,12 @@
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
sentIntents.add(pendingIntent);
}
+
MessageSentReceiver receiver = new MessageSentReceiver(
!TextUtils.isEmpty(contactName) ? contactName : phoneNumber,
messageParts.size());
- context.registerReceiver(receiver, new IntentFilter(ACTION_MESSAGE_SENT));
+ context.registerReceiver(receiver, new IntentFilter(ACTION_MESSAGE_SENT),
+ 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 661038b..d038a6e 100755
--- a/src/com/android/server/telecom/RespondViaSmsSettings.java
+++ b/src/com/android/server/telecom/RespondViaSmsSettings.java
@@ -89,6 +89,8 @@
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 c859fde..cdacab0 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_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.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.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 @@
*/
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 @@
RingtoneFactory ringtoneFactory,
Vibrator vibrator,
VibrationEffectProxy vibrationEffectProxy,
- InCallController inCallController) {
+ InCallController inCallController,
+ NotificationManager notificationManager,
+ AccessibilityManagerAdapter accessibilityManagerAdapter) {
mLock = new Object();
mSystemSettingsUtil = systemSettingsUtil;
@@ -202,6 +220,8 @@
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 @@
mIsHapticPlaybackSupportedByDevice =
mSystemSettingsUtil.isHapticPlaybackSupported(mContext);
+
+ mAudioManager = mContext.getSystemService(AudioManager.class);
}
@VisibleForTesting
@@ -220,194 +242,304 @@
mBlockOnRingingFuture = future;
}
+ @VisibleForTesting
+ public void setNotificationManager(NotificationManager notificationManager) {
+ mNotificationManager = notificationManager;
+ }
+
public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {
- if (foregroundCall == null) {
- Log.wtf(this, "startRinging called with null foreground call.");
- return false;
- }
-
- 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;
- }
-
- // 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));
-
- RingerAttributes attributes = null;
+ boolean deferBlockOnRingingFuture = false;
+ // try-finally to ensure that the block on ringing future is always called.
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);
- }
-
- if (attributes == null) {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "RingerAttributes error");
- return false;
- }
-
- if (attributes.isEndEarly()) {
- if (attributes.letDialerHandleRinging()) {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Dialer handles");
+ if (foregroundCall == null) {
+ Log.wtf(this, "startRinging called with null foreground call.");
+ return false;
}
- if (attributes.isSilentRingingRequested()) {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Silent ringing "
- + "requested");
- }
- if (mBlockOnRingingFuture != null) {
- mBlockOnRingingFuture.complete(null);
- }
- return attributes.shouldAcquireAudioFocus();
- }
- stopCallWaiting();
+ 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;
+ }
- 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;
+ // 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));
+
+ 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 == null) {
+ Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
+ "RingerAttributes error");
+ return false;
+ }
+
+ 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 (mVolumeShaperConfig == null) {
+
+ 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;
+ }
+
+ 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));
+ }
+
+ // 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();
- }
- 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);
- }
- 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);
+ 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 {
- Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
- "using audio-coupled haptics");
+ if (DEBUG_RINGER) {
+ Log.i(this, "Create ringer with custom vibration effect");
+ }
+ // Ramping ringtone is not enabled.
+ useCustomVibrationEffect = true;
}
- });
- if (mBlockOnRingingFuture != null) {
- mVibrateFuture.whenComplete((v, e) -> mBlockOnRingingFuture.complete(null));
- }
- } else {
- if (mBlockOnRingingFuture != null) {
- 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) {
- synchronized (mLock) {
- mAudioManager = mContext.getSystemService(AudioManager.class);
- if (isVibrationEnabled && !mIsVibrating && shouldRingForContact) {
- 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.");
- }
- mIsVibrating = true;
- mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
} else {
+ 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
+ }
+ }
+
+ 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 {
+ ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;
+ }
+ } else {
+ ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
+ foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
+ }
+
+ // 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, isVibrating=%b",
+ "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, "
+ + "isVibratorEnabled=%b",
mVibrator.hasVibrator(),
mSystemSettingsUtil.isRingVibrationEnabled(mContext),
- mAudioManager.getRingerMode(), mIsVibrating);
+ 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);
}
}
}
- 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;
+ /**
+ * 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) {
+ 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;
}
- if (effect == null) {
- effect = mDefaultVibrationEffect;
+ 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);
+ mIsVibrating = true;
+ mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
+ Log.i(this, "start vibration.");
+ }
+ // else stopped already: this isn't started unless a reservation was made.
}
- return effect;
+ }
+
+ private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
+ Uri ringtoneUri = ringtone.getUri();
+ if (ringtoneUri == null) {
+ return 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;
+ }
}
public void startCallWaiting(Call call) {
@@ -419,7 +551,8 @@
return;
}
- if (mInCallController.doesConnectedDialerSupportRinging()) {
+ if (mInCallController.doesConnectedDialerSupportRinging(
+ call.getUserHandleFromTargetPhoneAccount())) {
Log.addEvent(call, LogUtils.Events.SKIP_RINGING, "Dialer handles");
return;
}
@@ -443,6 +576,13 @@
}
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 @@
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,33 @@
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.v(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);
+
+ // query NotificationManager
+ boolean shouldRing = mNotificationManager.matchesCallFilter(peopleExtras);
+ // store the suppressed status in the call object
+ call.setCallIsSuppressedByDoNotDisturb(!shouldRing);
+
+ return shouldRing;
}
private boolean hasExternalRinger(Call foregroundCall) {
@@ -508,10 +659,8 @@
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 +675,19 @@
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 +695,32 @@
// 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.getUserHandleFromTargetPhoneAccount());
timer.record("letDialerHandleRinging");
+ boolean isWorkProfileInQuietMode =
+ isProfileInQuietMode(call.getUserHandleFromTargetPhoneAccount());
+ 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 +738,15 @@
.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 840d815..e0d3e1c 100644
--- a/src/com/android/server/telecom/RingerAttributes.java
+++ b/src/com/android/server/telecom/RingerAttributes.java
@@ -25,6 +25,7 @@
private String mInaudibleReason;
private boolean mShouldRingForContact;
private boolean mSilentRingingRequested;
+ private boolean mWorkProfileQuietMode;
public RingerAttributes.Builder setEndEarly(boolean endEarly) {
mEndEarly = endEarly;
@@ -61,10 +62,15 @@
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 @@
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 @@
mInaudibleReason = inaudibleReason;
mShouldRingForContact = shouldRingForContact;
mSilentRingingRequested = silentRingingRequested;
+ mWorkProfileQuietMode = workProfileQuietMode;
}
public boolean isEndEarly() {
@@ -115,4 +124,8 @@
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 5c46998..309c86e 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -65,26 +65,30 @@
}
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.getUserHandleFromTargetPhoneAccount());
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 @@
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 ba82a06..8fdfb11 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -41,7 +41,7 @@
* 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 @@
* 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 4a98d7b..ac35b3d 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -20,6 +20,7 @@
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 @@
}
@Override
- public String getDefaultCallRedirectionApp() {
+ public String getDefaultCallRedirectionApp(UserHandle userHandleForCallRedirection) {
if (mOverrideDefaultCallRedirectionApp != null) {
return mOverrideDefaultCallRedirectionApp;
}
- return getRoleManagerCallRedirectionApp();
+ return getRoleManagerCallRedirectionApp(userHandleForCallRedirection);
}
@Override
@@ -63,11 +64,11 @@
}
@Override
- public String getDefaultCallScreeningApp() {
+ public String getDefaultCallScreeningApp(UserHandle userHandleForCallScreening) {
if (mOverrideDefaultCallScreeningApp != null) {
return mOverrideDefaultCallScreeningApp;
}
- return getRoleManagerCallScreeningApp();
+ return getRoleManagerCallScreeningApp(userHandleForCallScreening);
}
@Override
@@ -118,9 +119,9 @@
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 @@
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 @@
pw.print("(override ");
pw.print(mOverrideDefaultCallRedirectionApp);
pw.print(") ");
- pw.print(getRoleManagerCallRedirectionApp());
+ pw.print(getRoleManagerCallRedirectionApp(Binder.getCallingUserHandle()));
}
pw.println();
@@ -193,7 +194,7 @@
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 af3493e..772335e 100644
--- a/src/com/android/server/telecom/StatusBarNotifier.java
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -29,7 +29,7 @@
*/
@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 @@
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 0000000..e899aff
--- /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/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index e1f2d08..0be90e0 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;
@@ -247,8 +249,14 @@
* 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 ee7aba6..99a8d3d 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -19,6 +19,7 @@
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_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.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.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 @@
}
}
+ 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 @@
Binder.restoreCallingIdentity(token);
}
if (isCallerSimCallManager(phoneAccountHandle)
- || canReadPhoneState(
+ || canReadPhoneState(
callingPackage,
callingFeatureId,
"getDefaultOutgoingPhoneAccount")) {
- return phoneAccountHandle;
+ return phoneAccountHandle;
}
return null;
}
@@ -181,6 +340,8 @@
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 @@
}
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 @@
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 @@
try {
return new ParceledListSlice<>(mPhoneAccountRegistrar
.getSelfManagedPhoneAccountsForPackage(callingPackage,
- callingUserHandle));
+ callingUserHandle));
} catch (Exception e) {
Log.e(this, e,
"getSelfManagedPhoneAccountsForPackage");
@@ -306,8 +470,8 @@
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 @@
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 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 @@
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 @@
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 @@
}
@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 @@
}
} 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 @@
}
@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 @@
}
} 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 @@
}
@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 @@
// 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 @@
.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 @@
}
@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 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 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 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 @@
@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 @@
/**
* @see android.telecom.TelecomManager#getDefaultPhoneApp
* @deprecated - Use {@link android.telecom.TelecomManager#getDefaultDialerPackage()}
- * instead.
+ * instead.
*/
@Override
public ComponentName getDefaultPhoneApp() {
@@ -803,18 +993,19 @@
/**
* @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 @@
/**
* @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 @@
* @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 @@
@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 @@
@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 @@
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 @@
@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 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 @@
}
}
+ 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 @@
/**
* @see android.telecom.TelecomManager#acceptRingingCall(int)
- *
*/
@Override
public void acceptRingingCallWithVideoState(String packageName, int videoState) {
@@ -1103,9 +1324,10 @@
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 @@
Log.endSession();
}
}
+
/**
* @see android.telecom.TelecomManager#handleMmi
*/
@@ -1158,7 +1381,7 @@
}
return retval;
- }finally {
+ } finally {
Log.endSession();
}
}
@@ -1199,7 +1422,7 @@
Binder.restoreCallingIdentity(token);
}
return retval;
- }finally {
+ } finally {
Log.endSession();
}
}
@@ -1282,9 +1505,10 @@
* @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 @@
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 @@
// 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 @@
* @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 @@
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 @@
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 @@
* @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 @@
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 @@
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 @@
enforceCallingPackage(callingPackage, "placeCall");
PhoneAccountHandle phoneAccountHandle = null;
- boolean clearPhoneAccountHandleExtra = false;
if (extras != null) {
phoneAccountHandle = extras.getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
@@ -1529,31 +1762,42 @@
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;
- }
+ // 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.");
+ }
- 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");
+ // 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
@@ -1584,16 +1828,12 @@
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),
true /* isLocalInvocation */);
} finally {
Binder.restoreCallingIdentity(token);
@@ -1633,10 +1873,11 @@
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 +1921,12 @@
}
/**
- * 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 +1940,21 @@
}
- 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 +1978,13 @@
* @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 +2011,7 @@
@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 +2036,7 @@
@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 +2149,12 @@
/**
* 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 +2165,18 @@
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.getUserHandleFromTargetPhoneAccount());
}
- mCallsManager.getInCallController().unbindFromServices();
+ for (UserHandle userHandle : userHandles) {
+ mCallsManager.getInCallController().unbindFromServices(userHandle);
+ }
});
}
} finally {
@@ -1930,7 +2186,8 @@
/**
* 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 +2381,8 @@
* 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 +2411,20 @@
}
};
+ 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 +2475,8 @@
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 +2513,14 @@
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 +2633,29 @@
}
}
+ // 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 +2687,89 @@
}
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 +2834,9 @@
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 +2855,18 @@
}
}
+ 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) {
@@ -2752,4 +3138,22 @@
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 237f039..8477d49 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -16,43 +16,49 @@
package com.android.server.telecom;
+import android.Manifest;
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+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.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.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.UserHandle;
-import android.os.UserManager;
-import android.telecom.Log;
-import android.telecom.PhoneAccountHandle;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
+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.
@@ -208,9 +214,14 @@
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 +280,15 @@
}
};
+ 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 +339,17 @@
}
};
+ 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();
mCallsManager = new CallsManager(
mContext,
mLock,
@@ -348,7 +379,14 @@
inCallControllerFactory,
callDiagnosticServiceController,
roleManagerAdapter,
- toastFactory);
+ toastFactory,
+ callEndpointControllerFactory,
+ callAnomalyWatchdog,
+ accessibilityManagerAdapter,
+ asyncTaskExecutor,
+ blockedNumbersAdapter,
+ transactionManager,
+ emergencyCallDiagnosticLogger);
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 36caa25..c5fdd4c 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -20,8 +20,8 @@
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 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 @@
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 0000000..f84b934
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -0,0 +1,76 @@
+/*
+ * 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.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 Map<PhoneAccountHandle, TransactionalServiceWrapper> lookupTable =
+ new HashMap<>();
+
+ public TransactionalServiceRepository() {
+ }
+
+ public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
+ (PhoneAccountHandle phoneAccountHandle, ICallEventCallback callEventCallback,
+ CallsManager callsManager, Call call) {
+
+ TransactionalServiceWrapper service = null;
+ if (!hasExistingServiceWrapper(phoneAccountHandle)) {
+ service = new TransactionalServiceWrapper(callEventCallback,
+ callsManager, phoneAccountHandle, call, this);
+ } else {
+ service = getTransactionalServiceWrapper(phoneAccountHandle);
+ if (service == null) {
+ throw new IllegalStateException("service is null");
+ } else {
+ service.trackCall(call);
+ }
+ }
+
+ lookupTable.put(phoneAccountHandle, service);
+
+ return service;
+ }
+
+ public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+ return lookupTable.get(pah);
+ }
+
+ public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ return lookupTable.containsKey(pah);
+ }
+
+ public boolean removeServiceWrapper(PhoneAccountHandle pah) {
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ lookupTable.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 0000000..d83e551
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -0,0 +1,690 @@
+/*
+ * 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.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * 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, IBinder.DeathRecipient {
+ 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 Hashtable<String, Call> mTrackedCalls = new Hashtable<>();
+ private final TelecomSystem.SyncRoot mLock;
+ private final String mPackageName;
+ // needs to be non-final for testing
+ private TransactionManager mTransactionManager;
+ private CallStreamingController mStreamingController;
+
+ 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();
+ }
+
+ @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);
+ }
+ }
+ }
+
+ public Call getCallById(String callId) {
+ synchronized (mLock) {
+ return mTrackedCalls.get(callId);
+ }
+ }
+
+ @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;
+ }
+
+ @Override
+ public void binderDied() {
+ // remove all tacked calls from CallsManager && frameworks side
+ for (String id : mTrackedCalls.keySet()) {
+ Call call = mTrackedCalls.get(id);
+ mCallsManager.markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.ERROR));
+ mCallsManager.removeCall(call);
+ // remove calls from Frameworks side
+ if (mICallEventCallback != null) {
+ try {
+ mICallEventCallback.removeCallFromTransactionalServiceWrapper(call.getId());
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+ }
+ mTrackedCalls.clear();
+ }
+
+ /***
+ *********************************************************************************************
+ ** 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(createStartStreamingTransaction(call), 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();
+ }
+ }
+ };
+
+ private 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());
+ // remove the call from this class/wrapper (server side)
+ untrackCall(call);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ 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);
+ }
+
+ /***
+ *********************************************************************************************
+ ** 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 **
+ *********************************************************************************************
+ */
+
+ private SerialTransaction createStartStreamingTransaction(Call call) {
+ // start streaming transaction flow:
+ // make sure there's no ongoing streaming call --> bind to EXO
+ // `-> change audio mode
+ // create list for multiple transactions
+ List<VoipCallTransaction> transactions = new ArrayList<>();
+
+ // add t1. make sure no ongoing streaming call
+ transactions.add(new CallStreamingController.QueryCallStreamingTransaction(mCallsManager));
+
+ // create list for parallel transactions
+ List<VoipCallTransaction> subTransactions = new ArrayList<>();
+ // add t2.1 bind to call streaming service
+ subTransactions.add(mStreamingController.getCallStreamingServiceTransaction(
+ mCallsManager.getContext(), this, call));
+ // add t2.2 audio route operations
+ subTransactions.add(new CallStreamingController.AudioInterceptionTransaction(call,
+ true, mLock));
+
+ // add t2
+ transactions.add(new ParallelTransaction(subTransactions, mLock));
+ // send off to Transaction Manager to process
+ return new SerialTransaction(transactions, mLock);
+ }
+
+ private VoipCallTransaction createStopStreamingTransaction(Call call) {
+ // TODO: implement this
+ // Stop streaming transaction flow:
+ List<VoipCallTransaction> transactions = new ArrayList<>();
+
+ // 1. unbind to call streaming service
+ transactions.add(mStreamingController.getUnbindStreamingServiceTransaction());
+ // 2. audio route operations
+ transactions.add(new CallStreamingController.AudioInterceptionTransaction(call,
+ false, mLock));
+ return new ParallelTransaction(transactions, mLock);
+ }
+
+
+ public void stopCallStreaming(Call call) {
+ if (call != null && call.isStreaming()) {
+ VoipCallTransaction stopStreamingTransaction = createStopStreamingTransaction(call);
+ addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 196c62f..473e7b9 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -223,10 +223,10 @@
}
for (LinkedHashMap.Entry<BluetoothDevice, Integer> entry : mGroupsByDevice.entrySet()) {
- if (Objects.equals(entry.getKey(),
+ if (Objects.equals(entry.getKey(),
mBluetoothLeAudioService.getConnectedGroupLeadDevice(entry.getValue()))) {
- devices.add(entry.getKey());
- }
+ devices.add(entry.getKey());
+ }
}
devices.removeIf(device -> !mLeAudioDevicesByAddress.containsValue(device));
return devices;
@@ -439,6 +439,7 @@
AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice();
if (audioDeviceInfo != null && audioDeviceInfo.getType()
== AudioDeviceInfo.TYPE_BLE_HEADSET) {
+ mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
mAudioManager.clearCommunicationDevice();
}
}
@@ -464,9 +465,7 @@
if (audioDeviceInfo != null && audioDeviceInfo.getType()
== AudioDeviceInfo.TYPE_HEARING_AID) {
mAudioManager.clearCommunicationDevice();
- mHearingAidSetAsCommunicationDevice = false;
}
- mHearingAidSetAsCommunicationDevice = false;
}
public boolean setLeAudioCommunicationDevice() {
@@ -647,6 +646,25 @@
}
}
+ public boolean isInbandRingingEnabled() {
+ BluetoothDevice activeDevice = mBluetoothRouteManager.getBluetoothAudioConnectedDevice();
+ Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
+ if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+ if (mBluetoothLeAudioService == null) {
+ Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
+ return false;
+ }
+ int groupId = mBluetoothLeAudioService.getGroupId(activeDevice);
+ return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
+ } else {
+ if (mBluetoothHeadset == null) {
+ Log.i(this, "isInbandRingingEnabled: no headset service available.");
+ return false;
+ }
+ return mBluetoothHeadset.isInbandRingingEnabled();
+ }
+ }
+
public void dump(IndentingPrintWriter pw) {
mLocalLog.dump(pw);
}
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 1945e28..7966f73 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -850,12 +850,7 @@
*/
@VisibleForTesting
public boolean isInbandRingingEnabled() {
- BluetoothHeadset bluetoothHeadset = mDeviceManager.getBluetoothHeadset();
- if (bluetoothHeadset == null) {
- Log.i(this, "isInbandRingingEnabled: no headset service available.");
- return false;
- }
- return bluetoothHeadset.isInbandRingingEnabled();
+ return mDeviceManager.isInbandRingingEnabled();
}
private boolean addDevice(String address) {
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 0000000..b8658d8
--- /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 84ce4d4..931d5bb 100644
--- a/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
+++ b/src/com/android/server/telecom/callfiltering/CallFilteringResult.java
@@ -29,6 +29,7 @@
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 @@
return this;
}
+ public Builder setDndSuppressed(boolean shouldPerformCheck) {
+ mDndSuppressed = shouldPerformCheck;
+ return this;
+ }
+
public Builder setShouldSilence(boolean shouldSilence) {
mShouldSilence = shouldSilence;
return this;
@@ -101,6 +107,7 @@
.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 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 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 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 @@
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 @@
.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 @@
.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 @@
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 @@
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 5f4c40b..f542fa2 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.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 @@
CallScreeningServiceConnection connection = new CallScreeningServiceConnection(
resultFuture);
if (!CallScreeningServiceHelper.bindCallScreeningService(mContext,
- mCallsManager.getCurrentUserHandle(), mPackageName, connection)) {
+ mCall.getUserHandleFromTargetPhoneAccount(), 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 0000000..f6ed646
--- /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 9fa864e..d79e80e 100644
--- a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraph.java
@@ -41,6 +41,7 @@
.setShouldReject(false)
.setShouldAddToCallLog(true)
.setShouldShowNotification(true)
+ .setDndSuppressed(false)
.build();
private final CallFilterResultCallback mListener;
@@ -143,6 +144,7 @@
.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 226382b..963e923 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -74,7 +74,7 @@
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 @@
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;
@@ -340,10 +340,10 @@
mPhoneAccountHandle, mRedirectionGatewayInfo, mSpeakerphoneOn,
mVideoState, mShouldCancelCall, mUiAction);
} else {
- performCarrierCallRedirection();
+ // Use the current user for carrier call redirection
+ performCarrierCallRedirection(UserHandle.CURRENT);
}
- }
- if (mIsCarrierRedirectionPending) {
+ } else if (mIsCarrierRedirectionPending) {
Log.addEvent(mCall, LogUtils.Events.REDIRECTION_COMPLETED_CARRIER);
mIsCarrierRedirectionPending = false;
mCallsManager.onCallRedirectionComplete(mCall, mDestinationUri,
@@ -357,39 +357,41 @@
/**
* 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 {
@@ -430,18 +432,22 @@
}
/**
- * 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 9771d65..7a2d415 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.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 @@
}
@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 9ad0da4..ef85fc7 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.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.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 @@
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 @@
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 1d85884..d7b2001 100644
--- a/src/com/android/server/telecom/components/UserCallActivity.java
+++ b/src/com/android/server/telecom/components/UserCallActivity.java
@@ -74,7 +74,8 @@
// 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 cad7b4c..a4602c1 100755
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -69,6 +69,7 @@
*
* @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 @@
* 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 @@
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 bc54e11..7345a67 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.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 @@
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)) {
@@ -154,7 +155,8 @@
}
};
registerReceiver(mBlockingStatusReceiver, new IntentFilter(
- BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED));
+ BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED),
+ Context.RECEIVER_EXPORTED);
getLoaderManager().initLoader(0, null, this);
}
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index c0bb56a..b1a1b0e 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -45,6 +45,7 @@
private static final String BLOCK_UNAVAILABLE_NUMBERS_KEY =
"block_unavailable_calls_setting";
private boolean mIsCombiningRestrictedAndUnknownOption = false;
+ private boolean mIsCombiningUnavailableAndUnknownOption = false;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -94,11 +95,17 @@
R.bool.combine_options_to_block_restricted_and_unknown_callers);
if (mIsCombiningRestrictedAndUnknownOption) {
Preference restricted_pref = findPreference(BLOCK_RESTRICTED_NUMBERS_KEY);
- Preference unavailable_pref = findPreference(BLOCK_UNAVAILABLE_NUMBERS_KEY);
screen.removePreference(restricted_pref);
- screen.removePreference(unavailable_pref);
Log.i(this, "onCreate: removed block restricted preference.");
}
+
+ mIsCombiningUnavailableAndUnknownOption = getResources().getBoolean(
+ R.bool.combine_options_to_block_unavailable_and_unknown_callers);
+ if (mIsCombiningUnavailableAndUnknownOption) {
+ Preference unavailable_pref = findPreference(BLOCK_UNAVAILABLE_NUMBERS_KEY);
+ screen.removePreference(unavailable_pref);
+ Log.i(this, "onCreate: removed block unavailable preference.");
+ }
}
/**
@@ -136,14 +143,20 @@
@Override
public boolean onPreferenceChange(Preference preference, Object objValue) {
- if (mIsCombiningRestrictedAndUnknownOption
- && preference.getKey().equals(BLOCK_UNKNOWN_NUMBERS_KEY)) {
- Log.i(this, "onPreferenceChange: changing %s and %s to %b",
- preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
- BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(), BLOCK_RESTRICTED_NUMBERS_KEY,
- (boolean) objValue);
- BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
- BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+ if (preference.getKey().equals(BLOCK_UNKNOWN_NUMBERS_KEY)) {
+ if (mIsCombiningRestrictedAndUnknownOption) {
+ Log.i(this, "onPreferenceChange: changing %s and %s to %b",
+ preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+ BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
+ BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+ }
+
+ if (mIsCombiningUnavailableAndUnknownOption) {
+ Log.i(this, "onPreferenceChange: changing %s and %s to %b",
+ preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+ BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
+ BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+ }
}
BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(), preference.getKey(),
(boolean) objValue);
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 0000000..3eb2321
--- /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 0000000..55b002e
--- /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 7a61460..e38178e 100644
--- a/src/com/android/server/telecom/ui/AudioProcessingNotification.java
+++ b/src/com/android/server/telecom/ui/AudioProcessingNotification.java
@@ -19,6 +19,7 @@
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 @@
showAudioProcessingNotification(call);
} else if (oldState == CallState.AUDIO_PROCESSING
&& newState != CallState.AUDIO_PROCESSING) {
- cancelAudioProcessingNotification();
+ cancelAudioProcessingNotification(
+ call.getUserHandleFromTargetPhoneAccount());
}
}
@@ -66,7 +68,8 @@
@Override
public void onCallRemoved(Call call) {
if (call == mCallInAudioProcessing) {
- cancelAudioProcessingNotification();
+ cancelAudioProcessingNotification(
+ call.getUserHandleFromTargetPhoneAccount());
}
}
@@ -76,7 +79,8 @@
* @param call The missed call.
*/
private void showAudioProcessingNotification(Call call) {
- Log.i(this, "showAudioProcessingNotification");
+ Log.i(this, "showAudioProcessingNotification for user = %s",
+ call.getUserHandleFromTargetPhoneAccount());
mCallInAudioProcessing = call;
Notification.Builder builder = new Notification.Builder(mContext,
@@ -92,12 +96,14 @@
Notification notification = builder.build();
- mNotificationManager.notify(
- NOTIFICATION_TAG, AUDIO_PROCESSING_NOTIFICATION_ID, notification);
+ mNotificationManager.notifyAsUser(NOTIFICATION_TAG, AUDIO_PROCESSING_NOTIFICATION_ID,
+ notification, mCallInAudioProcessing.getUserHandleFromTargetPhoneAccount());
}
/** 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/IncomingCallNotifier.java b/src/com/android/server/telecom/ui/IncomingCallNotifier.java
index 0c1c5a3..3b188d4 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.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 @@
showIncomingCallNotification(mIncomingCall);
} else if (hadIncomingCall && !hasIncomingCall) {
previousIncomingCall.removeListener(mCallListener);
- hideIncomingCallNotification();
+ hideIncomingCallNotification(
+ previousIncomingCall.getUserHandleFromTargetPhoneAccount());
}
}
}
private void showIncomingCallNotification(Call call) {
- Log.i(this, "showIncomingCallNotification showCall = %s", call);
+ Log.i(this, "showIncomingCallNotification showCall = %s for user = %s",
+ call, call.getUserHandleFromTargetPhoneAccount());
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.getUserHandleFromTargetPhoneAccount());
}
- 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/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
new file mode 100644
index 0000000..93d9836
--- /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 0000000..0cb7458
--- /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 0000000..e037a79
--- /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 0000000..6c4e8b7
--- /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 0000000..c0bb93d
--- /dev/null
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -0,0 +1,85 @@
+/*
+ * 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 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());
+ 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 0000000..a245c1c
--- /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 0000000..0b17da2
--- /dev/null
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -0,0 +1,131 @@
+/*
+ * 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.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());
+ 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 0000000..2762949
--- /dev/null
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import com.android.server.telecom.TelecomSystem;
+
+import java.util.List;
+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
+ mHandler.postDelayed(() -> {
+ if (mCompleted.getAndSet(true)) {
+ return;
+ }
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionTimeout(mTransactionName);
+ }
+ finish();
+ }, TIMEOUT_LIMIT);
+
+ 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) {
+ mHandler.post(() -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format("sub transaction %s failed",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ });
+ } else {
+ if (mCount.decrementAndGet() == 0) {
+ scheduleTransaction();
+ }
+ }
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName) {
+ mHandler.post(() -> {
+ VoipCallTransactionResult mainResult = new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format("sub transaction %s timed out",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ });
+ }
+ };
+ 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 0000000..f586cc3
--- /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 0000000..4c6d02e
--- /dev/null
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -0,0 +1,98 @@
+/*
+ * 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.TelecomSystem;
+
+import java.util.List;
+
+/**
+ * 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
+ mHandler.postDelayed(() -> {
+ if (mCompleted.getAndSet(true)) {
+ return;
+ }
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionTimeout(mTransactionName);
+ }
+ finish();
+ }, TIMEOUT_LIMIT);
+
+ 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) {
+ mHandler.post(() -> {
+ VoipCallTransactionResult mainResult =
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format("sub transaction %s failed",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ });
+ } else {
+ if (mSubTransactions.size() > 0) {
+ VoipCallTransaction transaction = mSubTransactions.remove(0);
+ transaction.setCompleteListener(this);
+ transaction.start();
+ } else {
+ scheduleTransaction();
+ }
+ }
+ }
+
+ @Override
+ public void onTransactionTimeout(String transactionName) {
+ mHandler.post(() -> {
+ VoipCallTransactionResult mainResult = new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED,
+ String.format("sub transaction %s timed out",
+ transactionName));
+ mCompleteListener.onTransactionCompleted(mainResult,
+ mTransactionName);
+ finish();
+ });
+ }
+ };
+ VoipCallTransaction transaction = mSubTransactions.remove(0);
+ transaction.setCompleteListener(subTransactionListener);
+ transaction.start();
+ } else {
+ scheduleTransaction();
+ }
+ }
+}
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 0000000..773dfb8
--- /dev/null
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -0,0 +1,129 @@
+/*
+ * 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 completed: with result=[%d]",
+ 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){
+ 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 0000000..2a81051
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -0,0 +1,287 @@
+/*
+ * 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.TelecomSystem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class VoipCallMonitor extends CallsManagerListenerBase {
+
+ private final List<Call> mPendingCalls;
+ // Same notification may be passed as different object in onNotificationPosted and
+ // onNotificationRemoved. Use its string as key to cache ongoing notifications.
+ private final Map<String, Call> mNotifications;
+ private final Map<PhoneAccountHandle, Set<Call>> mPhoneAccountHandleListMap;
+ 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<StatusBarNotification> mPendingSBN;
+
+ public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+ mContext = context;
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mPendingCalls = new ArrayList<>();
+ mPendingSBN = new ArrayList<>();
+ mNotifications = new HashMap<>();
+ mServices = new HashMap<>();
+ mPhoneAccountHandleListMap = 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)) {
+ boolean sbnMatched = false;
+ for (Call call : mPendingCalls) {
+ if (notificationMatchedCall(sbn, call)) {
+ mPendingCalls.remove(call);
+ mNotifications.put(sbn.toString(), call);
+ sbnMatched = true;
+ break;
+ }
+ }
+ if (!sbnMatched) {
+ // notification may posted before we started to monitor the call, cache
+ // this notification and try to match it later with new added call.
+ mPendingSBN.add(sbn);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ synchronized (mLock) {
+ mPendingSBN.remove(sbn);
+ if (mNotifications.isEmpty()) {
+ return;
+ }
+ Call call = mNotifications.getOrDefault(sbn.toString(), null);
+ if (call != null) {
+ mNotifications.remove(sbn.toString(), call);
+ stopFGSDelegation(call.getTargetPhoneAccount());
+ }
+ }
+ }
+ };
+
+ }
+
+ 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 = mPhoneAccountHandleListMap.computeIfAbsent(phoneAccountHandle,
+ k -> new HashSet<>());
+ callList.add(call);
+
+ mHandler.post(
+ () -> startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
+ call.getCallingPackageIdentity().mCallingPackageUid, call));
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (!call.isTransactionalCall()) {
+ return;
+ }
+
+ synchronized (mLock) {
+ stopMonitorWorks(call);
+ PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+ Set<Call> callList = mPhoneAccountHandleListMap.computeIfAbsent(phoneAccountHandle,
+ k -> new HashSet<>());
+ callList.remove(call);
+
+ if (callList.isEmpty()) {
+ stopFGSDelegation(phoneAccountHandle);
+ }
+ }
+ }
+
+ 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) {
+ Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+ mServices.put(handle, this);
+ startMonitorWorks(call);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
+ mServices.remove(handle);
+ }
+ };
+ try {
+ mActivityManagerInternal.startForegroundServiceDelegate(options, fgsConnection);
+ } catch (Exception e) {
+ Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void stopFGSDelegation(PhoneAccountHandle handle) {
+ synchronized (mLock) {
+ Log.i(this, "stopFGSDelegation of handle %s", handle);
+ Set<Call> calls = mPhoneAccountHandleListMap.get(handle);
+ if (calls != null) {
+ for (Call call : calls) {
+ stopMonitorWorks(call);
+ }
+ }
+ mPhoneAccountHandleListMap.remove(handle);
+
+ if (mActivityManagerInternal != null) {
+ ServiceConnection fgsConnection = mServices.get(handle);
+ if (fgsConnection != null) {
+ mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+ }
+ }
+ }
+ }
+
+ 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 (StatusBarNotification sbn : mPendingSBN) {
+ if (notificationMatchedCall(sbn, call)) {
+ mPendingSBN.remove(sbn);
+ mNotifications.put(sbn.toString(), call);
+ sbnMatched = true;
+ break;
+ }
+ }
+ if (!sbnMatched) {
+ // Only continue to
+ mPendingCalls.add(call);
+ mHandler.postDelayed(() -> {
+ synchronized (mLock) {
+ if (mPendingCalls.contains(call)) {
+ Log.i(this, "Notification for voip-call %s haven't "
+ + "posted in time, stop delegation.", call.getId());
+ stopFGSDelegation(call.getTargetPhoneAccount());
+ mPendingCalls.remove(call);
+ }
+ }
+ }, 5000L);
+ }
+ }
+ }
+
+ private void stopMonitorNotification(Call call) {
+ mPendingCalls.remove(call);
+ }
+
+ @VisibleForTesting
+ public void setActivityManagerInternal(ActivityManagerInternal ami) {
+ mActivityManagerInternal = ami;
+ }
+
+ @VisibleForTesting
+ public void setNotificationListenerService(NotificationListenerService listener) {
+ mNotificationListener = listener;
+ }
+
+ private boolean notificationMatchedCall(StatusBarNotification sbn, Call call) {
+ String packageName = sbn.getPackageName();
+ UserHandle userHandle = sbn.getUser();
+ PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+
+ return packageName != null &&
+ packageName.equals(call.getTargetPhoneAccount()
+ .getComponentName().getPackageName())
+ && userHandle != null
+ && userHandle.equals(accountHandle.getUserHandle());
+ }
+}
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 0000000..a1cc13c
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -0,0 +1,102 @@
+/*
+ * 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 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;
+ private 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
+ mHandler.postDelayed(() -> {
+ if (mCompleted.getAndSet(true)) {
+ return;
+ }
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionTimeout(mTransactionName);
+ }
+ finish();
+ }, TIMEOUT_LIMIT);
+
+ scheduleTransaction();
+ }
+
+ protected void scheduleTransaction() {
+ CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
+ future.thenComposeAsync(this::processTransaction,
+ new LoggedHandlerExecutor(mHandler, mTransactionName + "@"
+ + hashCode() + ".pT", mLock))
+ .thenApplyAsync(
+ (Function<VoipCallTransactionResult, Void>) result -> {
+ mCompleted.set(true);
+ if (mCompleteListener != null) {
+ mCompleteListener.onTransactionCompleted(result, mTransactionName);
+ }
+ finish();
+ return null;
+ });
+ }
+
+ 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 0000000..2916fc6
--- /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 dd8258a..645a42b 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 d26d629..98b879a 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 3939e78..9447ac8 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 0000000..7bb9830
--- /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 d4661ff..273b060 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 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 @@
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 75ceb62..475f255 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 @@
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 44410d2..5cdaf3d 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.TextView;
import android.widget.Toast;
-import com.android.server.telecom.testapps.R;
-
import java.util.Objects;
/**
@@ -66,6 +64,7 @@
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 @@
}));
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 @@
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 @@
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 @@
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 e6e35d8..3ef8fbb 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 @@
mCallList.notifyConnectionServiceFocusGained();
}
+ @SuppressWarnings("CatchAndPrintStackTrace")
private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming,
boolean isHandover) {
SelfManagedConnection connection = new SelfManagedConnection(mCallList,
@@ -101,11 +102,13 @@
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/transactionalVoipApp/Android.bp b/testapps/transactionalVoipApp/Android.bp
new file mode 100644
index 0000000..68089e2
--- /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 0000000..e4968db
--- /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 0000000..ed3ee45
--- /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 0000000..a4add51
--- /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 0000000..41558f2
--- /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 0000000..6006b12
--- /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 0000000..4f935bf
--- /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 0000000..54d467e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
@@ -0,0 +1,97 @@
+<?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/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"/>
+
+ <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 0000000..28f0744
--- /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 0000000..0129b46
--- /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 0000000..a0b39b4
--- /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 0000000..78abd1b
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-af/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-am/strings.xml b/testapps/transactionalVoipApp/res/values-am/strings.xml
new file mode 100644
index 0000000..2766bf8
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-am/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ar/strings.xml b/testapps/transactionalVoipApp/res/values-ar/strings.xml
new file mode 100644
index 0000000..8a42e30
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ar/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-as/strings.xml b/testapps/transactionalVoipApp/res/values-as/strings.xml
new file mode 100644
index 0000000..56014c4
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-as/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-az/strings.xml b/testapps/transactionalVoipApp/res/values-az/strings.xml
new file mode 100644
index 0000000..14af0ab
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-az/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..3c4019c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-be/strings.xml b/testapps/transactionalVoipApp/res/values-be/strings.xml
new file mode 100644
index 0000000..9decf62
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-be/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-bg/strings.xml b/testapps/transactionalVoipApp/res/values-bg/strings.xml
new file mode 100644
index 0000000..63b55f9
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-bg/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-bn/strings.xml b/testapps/transactionalVoipApp/res/values-bn/strings.xml
new file mode 100644
index 0000000..b03123a
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-bn/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-bs/strings.xml b/testapps/transactionalVoipApp/res/values-bs/strings.xml
new file mode 100644
index 0000000..e4cbb08
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-bs/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml
new file mode 100644
index 0000000..6780882
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ca/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-cs/strings.xml b/testapps/transactionalVoipApp/res/values-cs/strings.xml
new file mode 100644
index 0000000..46a938b
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-cs/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-da/strings.xml b/testapps/transactionalVoipApp/res/values-da/strings.xml
new file mode 100644
index 0000000..e857f3e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-da/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-de/strings.xml b/testapps/transactionalVoipApp/res/values-de/strings.xml
new file mode 100644
index 0000000..cf3116c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-de/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-el/strings.xml b/testapps/transactionalVoipApp/res/values-el/strings.xml
new file mode 100644
index 0000000..d838d2e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-el/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..5bfa1a1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rAU/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..1014001
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rCA/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..5bfa1a1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rGB/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..5bfa1a1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rIN/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..40b0016
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-en-rXC/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..3410a16
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-es-rUS/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-es/strings.xml b/testapps/transactionalVoipApp/res/values-es/strings.xml
new file mode 100644
index 0000000..2ce1e81
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-es/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-et/strings.xml b/testapps/transactionalVoipApp/res/values-et/strings.xml
new file mode 100644
index 0000000..477dec5
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-et/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-eu/strings.xml b/testapps/transactionalVoipApp/res/values-eu/strings.xml
new file mode 100644
index 0000000..962346f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-eu/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fa/strings.xml b/testapps/transactionalVoipApp/res/values-fa/strings.xml
new file mode 100644
index 0000000..bd9cddf
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fa/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fi/strings.xml b/testapps/transactionalVoipApp/res/values-fi/strings.xml
new file mode 100644
index 0000000..c95efcb
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fi/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..64df91c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-fr/strings.xml b/testapps/transactionalVoipApp/res/values-fr/strings.xml
new file mode 100644
index 0000000..f1d1bd7
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-fr/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-gl/strings.xml b/testapps/transactionalVoipApp/res/values-gl/strings.xml
new file mode 100644
index 0000000..76fbb34
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-gl/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-gu/strings.xml b/testapps/transactionalVoipApp/res/values-gu/strings.xml
new file mode 100644
index 0000000..b0066da
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-gu/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hi/strings.xml b/testapps/transactionalVoipApp/res/values-hi/strings.xml
new file mode 100644
index 0000000..a6e4a10
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hi/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hr/strings.xml b/testapps/transactionalVoipApp/res/values-hr/strings.xml
new file mode 100644
index 0000000..768d378
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hr/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hu/strings.xml b/testapps/transactionalVoipApp/res/values-hu/strings.xml
new file mode 100644
index 0000000..cda3b7e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hu/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-hy/strings.xml b/testapps/transactionalVoipApp/res/values-hy/strings.xml
new file mode 100644
index 0000000..b56941f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-hy/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-in/strings.xml b/testapps/transactionalVoipApp/res/values-in/strings.xml
new file mode 100644
index 0000000..e29fea7
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-in/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-is/strings.xml b/testapps/transactionalVoipApp/res/values-is/strings.xml
new file mode 100644
index 0000000..4ecb2ca
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-is/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-it/strings.xml b/testapps/transactionalVoipApp/res/values-it/strings.xml
new file mode 100644
index 0000000..bb83aa1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-it/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-iw/strings.xml b/testapps/transactionalVoipApp/res/values-iw/strings.xml
new file mode 100644
index 0000000..4de997e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-iw/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ja/strings.xml b/testapps/transactionalVoipApp/res/values-ja/strings.xml
new file mode 100644
index 0000000..a5e8251
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ja/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ka/strings.xml b/testapps/transactionalVoipApp/res/values-ka/strings.xml
new file mode 100644
index 0000000..671cffb
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ka/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-kk/strings.xml b/testapps/transactionalVoipApp/res/values-kk/strings.xml
new file mode 100644
index 0000000..2713491
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-kk/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-km/strings.xml b/testapps/transactionalVoipApp/res/values-km/strings.xml
new file mode 100644
index 0000000..13f4983
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-km/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-kn/strings.xml b/testapps/transactionalVoipApp/res/values-kn/strings.xml
new file mode 100644
index 0000000..b994f92
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-kn/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ko/strings.xml b/testapps/transactionalVoipApp/res/values-ko/strings.xml
new file mode 100644
index 0000000..9eb4556
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ko/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ky/strings.xml b/testapps/transactionalVoipApp/res/values-ky/strings.xml
new file mode 100644
index 0000000..577dcda
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ky/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-lo/strings.xml b/testapps/transactionalVoipApp/res/values-lo/strings.xml
new file mode 100644
index 0000000..69126d9
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-lo/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-lt/strings.xml b/testapps/transactionalVoipApp/res/values-lt/strings.xml
new file mode 100644
index 0000000..91e51fe
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-lt/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-lv/strings.xml b/testapps/transactionalVoipApp/res/values-lv/strings.xml
new file mode 100644
index 0000000..ae6896f
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-lv/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-mk/strings.xml b/testapps/transactionalVoipApp/res/values-mk/strings.xml
new file mode 100644
index 0000000..8501eaf
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-mk/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ml/strings.xml b/testapps/transactionalVoipApp/res/values-ml/strings.xml
new file mode 100644
index 0000000..67e4e34
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ml/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-mn/strings.xml b/testapps/transactionalVoipApp/res/values-mn/strings.xml
new file mode 100644
index 0000000..e4b6f36
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-mn/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-mr/strings.xml b/testapps/transactionalVoipApp/res/values-mr/strings.xml
new file mode 100644
index 0000000..dfb3184
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-mr/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ms/strings.xml b/testapps/transactionalVoipApp/res/values-ms/strings.xml
new file mode 100644
index 0000000..3005391
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ms/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-my/strings.xml b/testapps/transactionalVoipApp/res/values-my/strings.xml
new file mode 100644
index 0000000..818a3f7
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-my/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-nb/strings.xml b/testapps/transactionalVoipApp/res/values-nb/strings.xml
new file mode 100644
index 0000000..ab0353d
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-nb/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ne/strings.xml b/testapps/transactionalVoipApp/res/values-ne/strings.xml
new file mode 100644
index 0000000..3a12a70
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ne/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-nl/strings.xml b/testapps/transactionalVoipApp/res/values-nl/strings.xml
new file mode 100644
index 0000000..7c9ce32
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-nl/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-or/strings.xml b/testapps/transactionalVoipApp/res/values-or/strings.xml
new file mode 100644
index 0000000..7a805f4
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-or/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pa/strings.xml b/testapps/transactionalVoipApp/res/values-pa/strings.xml
new file mode 100644
index 0000000..8293899
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pa/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pl/strings.xml b/testapps/transactionalVoipApp/res/values-pl/strings.xml
new file mode 100644
index 0000000..3cb8ac4
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pl/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..6c4f149
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pt-rPT/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-pt/strings.xml b/testapps/transactionalVoipApp/res/values-pt/strings.xml
new file mode 100644
index 0000000..97bba50
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-pt/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ro/strings.xml b/testapps/transactionalVoipApp/res/values-ro/strings.xml
new file mode 100644
index 0000000..bb630a8
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ro/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ru/strings.xml b/testapps/transactionalVoipApp/res/values-ru/strings.xml
new file mode 100644
index 0000000..87c06f1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ru/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-si/strings.xml b/testapps/transactionalVoipApp/res/values-si/strings.xml
new file mode 100644
index 0000000..c28e166
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-si/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sk/strings.xml b/testapps/transactionalVoipApp/res/values-sk/strings.xml
new file mode 100644
index 0000000..5e76289
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sk/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sl/strings.xml b/testapps/transactionalVoipApp/res/values-sl/strings.xml
new file mode 100644
index 0000000..435eac9
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sl/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sq/strings.xml b/testapps/transactionalVoipApp/res/values-sq/strings.xml
new file mode 100644
index 0000000..3d18edf
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sq/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sr/strings.xml b/testapps/transactionalVoipApp/res/values-sr/strings.xml
new file mode 100644
index 0000000..df6a08b
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sr/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sv/strings.xml b/testapps/transactionalVoipApp/res/values-sv/strings.xml
new file mode 100644
index 0000000..51d300a
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sv/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-sw/strings.xml b/testapps/transactionalVoipApp/res/values-sw/strings.xml
new file mode 100644
index 0000000..3ad2501
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-sw/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ta/strings.xml b/testapps/transactionalVoipApp/res/values-ta/strings.xml
new file mode 100644
index 0000000..884291d
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ta/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-te/strings.xml b/testapps/transactionalVoipApp/res/values-te/strings.xml
new file mode 100644
index 0000000..b926d1a
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-te/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-th/strings.xml b/testapps/transactionalVoipApp/res/values-th/strings.xml
new file mode 100644
index 0000000..a1a9803
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-th/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-tl/strings.xml b/testapps/transactionalVoipApp/res/values-tl/strings.xml
new file mode 100644
index 0000000..d3399ff
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-tl/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-tr/strings.xml b/testapps/transactionalVoipApp/res/values-tr/strings.xml
new file mode 100644
index 0000000..d9a94ab
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-tr/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-uk/strings.xml b/testapps/transactionalVoipApp/res/values-uk/strings.xml
new file mode 100644
index 0000000..e08728c
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-uk/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-ur/strings.xml b/testapps/transactionalVoipApp/res/values-ur/strings.xml
new file mode 100644
index 0000000..e0e0c6e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-ur/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-uz/strings.xml b/testapps/transactionalVoipApp/res/values-uz/strings.xml
new file mode 100644
index 0000000..5421322
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-uz/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-vi/strings.xml b/testapps/transactionalVoipApp/res/values-vi/strings.xml
new file mode 100644
index 0000000..88362e4
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-vi/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..4b816ba
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zh-rCN/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..5b80831
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zh-rHK/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</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 0000000..b8a2045
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zh-rTW/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values-zu/strings.xml b/testapps/transactionalVoipApp/res/values-zu/strings.xml
new file mode 100644
index 0000000..8e14895
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values-zu/strings.xml
@@ -0,0 +1,35 @@
+<?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>
+</resources>
diff --git a/testapps/transactionalVoipApp/res/values/strings.xml b/testapps/transactionalVoipApp/res/values/strings.xml
new file mode 100644
index 0000000..23a5118
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?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>
+
+</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 0000000..b503e94
--- /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 0000000..b868b70
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -0,0 +1,268 @@
+/*
+ * 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.graphics.Color;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaPlayer;
+import android.net.StringNetworkSpecifier;
+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();
+ 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:");
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ 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("]");
+ view.setText(sb.toString());
+ }
+
+ private void addCall() {
+ mVoipCall = new MyVoipCall("123");
+
+ CallAttributes callAttributes =
+ new CallAttributes.Builder(
+ Utils.PHONE_ACCOUNT_HANDLE,
+ mCallDirection,
+ "Alan Turing",
+ Uri.parse("tel:6506959001")).build();
+
+ mTelecomManager.addCall(callAttributes, Runnable::run,
+ new OutcomeReceiver<CallControl, CallException>() {
+ @Override
+ public void onResult(CallControl callControl) {
+ Log.i(TAG, "addCall: onResult: callback fired");
+ 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);
+ } catch (IllegalArgumentException 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 0000000..e2b5b14
--- /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 0000000..98de790
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
@@ -0,0 +1,136 @@
+/*
+ * 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.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 sEXTRAS_KEY = "ExtrasKey";
+ public static final String sCALL_DIRECTION_KEY = "CallDirectionKey";
+ public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
+ 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("Tom Stu").setImportant(true).build(),
+ pendingAnswer, pendingReject)
+ )
+ .setFullScreenIntent(pendingAnswer, true)
+ .build();
+
+ return callStyleNotification;
+ }
+
+ 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 0000000..ae7d9d0
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
@@ -0,0 +1,147 @@
+/*
+ * 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) {
+ mNotificationManager.notify(123456,
+ Utils.createCallStyleNotification(getApplicationContext()));
+ 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. ");
+ super.onDestroy();
+ }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index e8c69d4..4ca6030 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 049a501..9047da3 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -44,6 +44,7 @@
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.telecom.Call;
import android.telecom.CallAudioState;
@@ -87,6 +88,7 @@
*/
@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";
@@ -327,7 +329,7 @@
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 +394,7 @@
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 +444,7 @@
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 +496,7 @@
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);
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index c1025e1..c37d136 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -16,12 +16,15 @@
package com.android.server.telecom.tests;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
import android.content.Intent;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
@@ -43,12 +46,14 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -66,6 +71,7 @@
@Mock BluetoothHearingAid mBluetoothHearingAid;
@Mock BluetoothLeAudio mBluetoothLeAudio;
@Mock AudioManager mockAudioManager;
+ @Mock AudioDeviceInfo mSpeakerInfo;
BluetoothDeviceManager mBluetoothDeviceManager;
BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -117,6 +123,8 @@
ArgumentCaptor.forClass(BluetoothLeAudio.Callback.class);
mBluetoothDeviceManager.setLeAudioServiceForTesting(mBluetoothLeAudio);
verify(mBluetoothLeAudio).registerCallback(any(), leAudioCallbacksTest.capture());
+
+ when(mSpeakerInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
}
@Override
@@ -500,14 +508,39 @@
when(mockAudioManager.getAvailableCommunicationDevices())
.thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
+ .thenReturn(true);
mBluetoothDeviceManager.setHearingAidCommunicationDevice();
- when(mockAudioManager.getCommunicationDevice()).thenReturn(null);
+ when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
}
+ @SmallTest
+ @Test
+ public void testInBandRingingEnabledForLeDevice() {
+ when(mBluetoothHeadset.isInbandRingingEnabled()).thenReturn(false);
+ when(mBluetoothLeAudio.isInbandRingtoneEnabled(1)).thenReturn(true);
+ when(mBluetoothLeAudio.getGroupId(eq(device3))).thenReturn(1);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+ BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
+ BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device3, 1);
+ when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
+ when(mRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(device3);
+ when(mRouteManager.isCachedLeAudioDevice(eq(device3))).thenReturn(true);
+ assertEquals(3, mBluetoothDeviceManager.getNumConnectedDevices());
+ assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled());
+ }
+
private Intent buildConnectionActionIntent(int state, BluetoothDevice device, int deviceType) {
String intentString;
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 0000000..7e197fe
--- /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 0000000..acb913e
--- /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 4adafc8..3d06ad0 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.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 @@
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 38f58fd..d0a1d8b 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.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 @@
@Mock private SystemStateHelper mSystemStateHelper;
@Mock private AudioManager mAudioManager;
@Mock private CallAudioManager mCallAudioManager;
+ @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
private HandlerThread mTestThread;
@@ -58,6 +60,8 @@
mTestThread = new HandlerThread("CallAudioModeStateMachineTest");
mTestThread.start();
super.setUp();
+ when(mCallAudioManager.getCallAudioRouteStateMachine())
+ .thenReturn(mCallAudioRouteStateMachine);
}
@Override
@@ -102,6 +106,64 @@
@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(false)
+ .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 0000000..dfe1483
--- /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 6092293..569c487 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 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 @@
@Mock ConnectionServiceWrapper mockConnectionServiceWrapper;
@Mock WiredHeadsetManager mockWiredHeadsetManager;
@Mock StatusBarNotifier mockStatusBarNotifier;
+ @Mock Call fakeSelfManagedCall;
@Mock Call fakeCall;
@Mock CallAudioManager mockCallAudioManager;
@@ -116,11 +119,16 @@
};
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 3eacc3a..cf684de 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.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 @@
};
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 @@
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 @@
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 0000000..2613206
--- /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 0000000..f4008aa
--- /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 0000000..fa22877
--- /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/CallFailureCauseTest.java b/tests/src/com/android/server/telecom/tests/CallFailureCauseTest.java
new file mode 100644
index 0000000..7c0f649
--- /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/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index f2fe045..01446d1 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 @@
}
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 @@
}
private void disableUserDefinedCallRedirectionService() {
- when(mCallRedirectionProcessorHelper.getUserDefinedCallRedirectionService()).thenReturn(
- null);
+ when(mCallRedirectionProcessorHelper
+ .getUserDefinedCallRedirectionService(any(UserHandle.class)))
+ .thenReturn(null);
}
private void disableCarrierCallRedirectionService() {
@@ -193,9 +195,9 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 09ba47e..d95a0e2 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.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 @@
.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 @@
when(mCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
when(mCall.getId()).thenReturn(CALL_ID);
+ when(mCall.getUserHandleFromTargetPhoneAccount()).
+ thenReturn(PA_HANDLE.getUserHandle());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mContext.getSystemService(TelecomManager.class))
.thenReturn(mTelecomManager);
@@ -132,7 +139,7 @@
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 @@
@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 @@
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 @@
.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 d326a29..997e7dd 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 @@
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 @@
@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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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,22 +291,7 @@
@Test
@SmallTest
public void testCanNotPullCallDuringEmergencyCall() {
- 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.setConnectionService(mMockConnectionService);
call.setConnectionProperties(Connection.PROPERTY_IS_EXTERNAL_CALL);
call.setConnectionCapabilities(Connection.CAPABILITY_CAN_PULL_CALL);
@@ -318,6 +307,22 @@
@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,
@@ -334,16 +339,415 @@
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());
+
+ 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);
+ assertEquals(mMockConnectionService, call.getConnectionServiceWrapper());
+ call.setIsTransactionalCall(true);
+ assertTrue(call.isTransactionalCall());
+ assertNull(call.getConnectionServiceWrapper());
+ call.setTransactionServiceWrapper(mMockTransactionalService);
+ assertEquals(mMockTransactionalService, call.getTransactionServiceWrapper());
+ }
+
+ @Test
+ public void testCallEventCallbacksWereCalled() {
+ 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);
+
+ // 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 e7d0a26..22a850f 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,27 @@
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.provider.Telephony;
+import android.telecom.CallException;
+import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -61,18 +73,24 @@
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 +99,9 @@
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 +114,7 @@
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 +122,12 @@
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.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 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,6 +153,7 @@
@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 PhoneAccountHandle SIM_1_HANDLE = new PhoneAccountHandle(
ComponentName.unflattenFromString("com.foo/.Blah"), "Sim1");
@@ -147,12 +170,21 @@
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 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,14 +196,19 @@
.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 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");
- private static final Map<Uri, PhoneAccountHandle> CONTACT_PREFERRED_ACCOUNT =
- new HashMap<Uri, PhoneAccountHandle>() {{
- put(TEST_ADDRESS2, SIM_1_HANDLE);
- put(TEST_ADDRESS3, SIM_2_HANDLE);
- }};
+ private static final Map<Uri, PhoneAccountHandle> CONTACT_PREFERRED_ACCOUNT = Map.of(
+ 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() { };
@@ -201,6 +238,8 @@
@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;
@@ -211,6 +250,13 @@
@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;
private CallsManager mCallsManager;
@@ -227,8 +273,10 @@
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());
@@ -239,6 +287,9 @@
.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,
@@ -268,7 +319,15 @@
mInCallControllerFactory,
mCallDiagnosticServiceController,
mRoleManagerAdapter,
- mToastFactory);
+ mToastFactory,
+ mCallEndpointControllerFactory,
+ mCallAnomalyWatchdog,
+ mAccessibilityManagerAdapter,
+ // Just do async tasks synchronously to support testing.
+ command -> command.run(),
+ mBlockedNumbersAdapter,
+ TransactionManager.getTestInstance(),
+ mEmergencyCallDiagnosticLogger);
when(mPhoneAccountRegistrar.getPhoneAccount(
eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -293,6 +352,26 @@
assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null, false, false).size());
}
+ private Call constructOngoingCall(String callId, PhoneAccountHandle phoneAccountHandle) {
+ Call ongoingCall = new Call(
+ callId,
+ mContext,
+ mCallsManager,
+ mLock,
+ null /* ConnectionServiceRepository */,
+ mPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null /* connectionManagerPhoneAccountHandle */,
+ 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.
@@ -303,23 +382,7 @@
public void testConstructPossiblePhoneAccountsMultiSimActive() throws Exception {
setupMsimAccounts();
- Call ongoingCall = new Call(
- "1", /* callId */
- mContext,
- mCallsManager,
- mLock,
- null /* ConnectionServiceRepository */,
- mPhoneNumberUtilsAdapter,
- TEST_ADDRESS,
- null /* GatewayInfo */,
- null /* connectionManagerPhoneAccountHandle */,
- SIM_2_HANDLE,
- Call.CALL_DIRECTION_INCOMING,
- false /* shouldAttachToExistingConnection*/,
- false /* isConference */,
- mClockProxy,
- mToastFactory);
- ongoingCall.setState(CallState.ACTIVE, "just cuz");
+ Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
mCallsManager.addCall(ongoingCall);
List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
@@ -342,6 +405,47 @@
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);
@@ -385,7 +489,7 @@
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(
@@ -409,7 +513,7 @@
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(
@@ -433,8 +537,8 @@
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 */)
@@ -457,11 +561,11 @@
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 */)
@@ -483,7 +587,7 @@
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(
@@ -504,7 +608,7 @@
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(
@@ -599,6 +703,54 @@
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() {
@@ -677,7 +829,7 @@
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.
@@ -885,7 +1037,7 @@
mCallsManager.markCallAsActive(newCall);
// THEN the ongoing call is held
- verify(ongoingCall).hold();
+ verify(ongoingCall).hold(anyString());
verifyFocusRequestAndExecuteCallback(newCall);
// and the new call is active
@@ -944,6 +1096,43 @@
@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();
@@ -1143,7 +1332,7 @@
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);
@@ -1171,13 +1360,132 @@
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);
@@ -1192,7 +1500,7 @@
@SmallTest
@Test
- public void testMakeRoomForEmergencyDuringIncomingCall() {
+ public void testMakeRoomForEmergencyCallDuringIncomingCall() {
Call ongoingCall = addSpyCall(SIM_2_HANDLE, CallState.RINGING);
Call newEmergencyCall = createCall(SIM_1_HANDLE, CallState.NEW);
@@ -1255,21 +1563,230 @@
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.
@@ -1401,6 +1918,7 @@
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)
@@ -1479,7 +1997,7 @@
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
@@ -1606,7 +2124,35 @@
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
@@ -1647,6 +2193,79 @@
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);
@@ -1678,6 +2297,43 @@
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() {
@@ -1695,6 +2351,672 @@
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.getUserHandleFromTargetPhoneAccount()))
+ .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() 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 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));
+ }
+
private Call addSpyCall() {
return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
}
@@ -1767,6 +3089,10 @@
mClockProxy,
mToastFactory);
ongoingCall.setState(initialState, "just cuz");
+ if (targetPhoneAccount == SELF_MANAGED_HANDLE
+ || targetPhoneAccount == SELF_MANAGED_2_HANDLE) {
+ ongoingCall.setIsSelfManaged(true);
+ }
return ongoingCall;
}
@@ -1782,9 +3108,15 @@
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 4ad46ae..6056747 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 @@
}
/**
+ * 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 477ca9f..cc22de2 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.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.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.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 @@
* 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 @@
}
@Override
+ public Context createAttributionContext(String attributionTag) { return this; }
+
+ @Override
public String getPackageName() {
return "com.android.server.telecom.tests";
}
@@ -199,6 +211,8 @@
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 @@
return mPermissionCheckerManager;
case Context.SENSOR_PRIVACY_SERVICE:
return mSensorPrivacyManager;
+ case Context.ACCESSIBILITY_SERVICE:
+ return mAccessibilityManager;
default:
return null;
}
@@ -262,8 +278,14 @@
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 @@
}
@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 @@
@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 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 @@
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 @@
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 @@
return mTelephonyManager;
}
+ public AudioManager getAudioManager() {
+ return mAudioManager;
+ }
+
public CarrierConfigManager getCarrierConfigManager() {
return mCarrierConfigManager;
}
@@ -771,6 +818,10 @@
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 6e6646f..c8da78c 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.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;
@@ -343,6 +344,18 @@
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 { }
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index cb376af..dbcab66 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.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 @@
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 @@
SubscriptionManager.INVALID_SIM_SLOT_INDEX);
}
});
- when(mMockAccountRegistrar.getAllPhoneAccountsOfCurrentUser()).thenReturn(phoneAccounts);
+ when(mMockAccountRegistrar.getAllPhoneAccounts(any(UserHandle.class), anyBoolean()))
+ .thenReturn(phoneAccounts);
+ when(mMockCall.getUserHandleFromTargetPhoneAccount()).
+ thenReturn(Binder.getCallingUserHandle());
}
@Override
@@ -200,7 +208,7 @@
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 @@
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 @@
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 @@
// 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 @@
}
/**
+ * 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 @@
"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 @@
"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 @@
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 @@
"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 @@
}
/**
+ * 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 @@
"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 @@
"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 @@
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 @@
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 @@
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 @@
}
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 @@
}
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 @@
}
private PhoneAccountHandle getNewTargetPhoneAccountHandle(String id) {
- PhoneAccountHandle pAHandle = makeQuickAccountHandle(id);
+ PhoneAccountHandle pAHandle = makeQuickAccountHandle(id, null);
givePhoneAccountBindPermission(pAHandle);
return pAHandle;
}
@@ -739,7 +815,7 @@
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 @@
}
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 @@
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/DndCallFilteringTests.java b/tests/src/com/android/server/telecom/tests/DndCallFilteringTests.java
new file mode 100644
index 0000000..4885d61
--- /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 0000000..3cb8196
--- /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 0000000..380e327
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+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.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Matchers.eq;
+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.verify;
+import static org.mockito.Mockito.when;
+
+@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;
+
+ @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);
+ }
+
+ @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);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
index 6d15e60..ce23724 100644
--- a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
+++ b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
@@ -16,20 +16,34 @@
package com.android.server.telecom.tests;
+import android.content.Intent;
+import android.media.session.MediaSession;
+import android.telecom.CallEndpoint;
+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 +52,7 @@
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 +62,15 @@
@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
@@ -59,8 +81,9 @@
}
/**
- * Nominal case; just add a call and remove it.
+ * Nominal case; just add a call and remove it; this happens when the audio state is earpiece.
*/
+ @SmallTest
@Test
public void testAddCall() {
Call regularCall = getRegularCall();
@@ -68,19 +91,101 @@
when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
mHeadsetMediaButton.onCallAdded(regularCall);
waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+ // Report that the endpoint is earpiece and other routes that don't matter
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Earpiece", CallEndpoint.TYPE_EARPIECE));
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Speaker", CallEndpoint.TYPE_SPEAKER));
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("BT", CallEndpoint.TYPE_BLUETOOTH));
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+ // ... and thus we see how the original code isn't amenable to tests.
+ when(mMediaSessionAdapter.isActive()).thenReturn(false);
+
+ // Still should not have done anything; we never hit wired headset
+ mHeadsetMediaButton.onCallRemoved(regularCall);
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(false));
+ }
+
+ /**
+ * Call is added and then routed to headset after call start
+ */
+ @SmallTest
+ @Test
+ public void testAddCallThenRouteToHeadset() {
+ Call regularCall = getRegularCall();
+
+ mHeadsetMediaButton.onCallAdded(regularCall);
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
verify(mMediaSessionAdapter).setActive(eq(true));
+
// ... and thus we see how the original code isn't amenable to tests.
when(mMediaSessionAdapter.isActive()).thenReturn(true);
- when(mMockCallsManager.hasAnyCalls()).thenReturn(false);
+ // Remove the one call; we should release the session.
mHeadsetMediaButton.onCallRemoved(regularCall);
waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
verify(mMediaSessionAdapter).setActive(eq(false));
+ when(mMediaSessionAdapter.isActive()).thenReturn(false);
+
+ // Add a new call; make sure we go active once more.
+ mHeadsetMediaButton.onCallAdded(regularCall);
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, times(2)).setActive(eq(true));
+ }
+
+ /**
+ * Call is added and then routed to headset after call start
+ */
+ @SmallTest
+ @Test
+ public void testAddCallThenRouteToHeadsetAndBack() {
+ Call regularCall = getRegularCall();
+
+ when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
+ mHeadsetMediaButton.onCallAdded(regularCall);
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter, never()).setActive(eq(true));
+
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter).setActive(eq(true));
+
+ // ... and thus we see how the original code isn't amenable to tests.
+ when(mMediaSessionAdapter.isActive()).thenReturn(true);
+
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Earpiece", CallEndpoint.TYPE_EARPIECE));
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ verify(mMediaSessionAdapter).setActive(eq(false));
+ when(mMediaSessionAdapter.isActive()).thenReturn(false);
+
+ // Remove the one call; we should not release again.
+ mHeadsetMediaButton.onCallRemoved(regularCall);
+ waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
+ // Remember, mockito counts total invocations; we should have went active once and then
+ // inactive again when we hit earpiece.
+ verify(mMediaSessionAdapter, times(1)).setActive(eq(true));
+ verify(mMediaSessionAdapter, times(1)).setActive(eq(false));
}
/**
* Test a case where a regular call becomes an external call, and back again.
*/
+ @SmallTest
@Test
public void testRegularCallThatBecomesExternal() {
Call regularCall = getRegularCall();
@@ -88,6 +193,8 @@
// Start with a regular old call.
when(mMockCallsManager.hasAnyCalls()).thenReturn(true);
mHeadsetMediaButton.onCallAdded(regularCall);
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
verify(mMediaSessionAdapter).setActive(eq(true));
when(mMediaSessionAdapter.isActive()).thenReturn(true);
@@ -99,6 +206,7 @@
// Expect to set session inactive.
waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
verify(mMediaSessionAdapter).setActive(eq(false));
+ when(mMediaSessionAdapter.isActive()).thenReturn(false);
// For good measure lets make it non-external again.
when(regularCall.isExternalCall()).thenReturn(false);
@@ -106,7 +214,88 @@
mHeadsetMediaButton.onExternalCallChanged(regularCall, false);
// Expect to set session active.
waitForHandlerAction(mHeadsetMediaButton.getHandler(), TEST_TIMEOUT_MILLIS);
- verify(mMediaSessionAdapter).setActive(eq(true));
+ verify(mMediaSessionAdapter, times(2)).setActive(eq(true));
+ }
+
+ @MediumTest
+ @Test
+ public void testExternalCallNotChangesState() {
+ Call externalCall = getRegularCall();
+ when(externalCall.isExternalCall()).thenReturn(true);
+
+ mHeadsetMediaButton.onCallAdded(externalCall);
+ mHeadsetMediaButton.onCallEndpointChanged(
+ new CallEndpoint("Wired Headset", CallEndpoint.TYPE_WIRED_HEADSET));
+ 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());
}
/**
@@ -117,4 +306,15 @@
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 3d387a8..16fd630 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -52,10 +52,12 @@
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 +65,10 @@
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 +76,7 @@
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 +92,7 @@
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 +100,7 @@
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 +118,20 @@
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 +150,41 @@
@Mock Analytics.CallInfoImpl mCallInfo;
@Mock NotificationManager mNotificationManager;
@Mock PermissionInfo mMockPermissionInfo;
+ @Mock InCallController.InCallServiceInfo mInCallServiceInfo;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
+ @Mock UserManager mMockUserManager;
+ @Mock UserInfo mMockUserInfo;
@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,6 +192,7 @@
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
@@ -191,6 +204,8 @@
super.setUp();
MockitoAnnotations.initMocks(this);
when(mMockCall.getAnalytics()).thenReturn(new Analytics.CallInfo());
+ when(mMockCall.getUserHandleFromTargetPhoneAccount()).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 +229,12 @@
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).registerReceiver(any(BroadcastReceiver.class),
+ any(IntentFilter.class));
ArgumentCaptor<SystemStateHelper.SystemStateListener> systemStateListenerArgumentCaptor
= ArgumentCaptor.forClass(SystemStateHelper.SystemStateListener.class);
@@ -273,6 +294,11 @@
.thenReturn(PackageManager.PERMISSION_DENIED);
when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0));
+
+ when(mMockContext.getSystemService(eq(Context.USER_SERVICE))).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 +311,15 @@
@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 +383,7 @@
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 +395,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -393,7 +429,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -421,7 +457,7 @@
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 +481,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -467,6 +503,9 @@
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);
@@ -475,7 +514,7 @@
.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 +542,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -526,6 +565,88 @@
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.getUserHandleFromTargetPhoneAccount()).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.getUserHandleFromTargetPhoneAccount()).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_UserAssociatedWithCallNotInQuietMode_EmergCallInCallUi_BindsToAssociatedUser()
+ throws Exception {
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.getUserHandleFromTargetPhoneAccount()).thenReturn(DUMMY_USER_HANDLE);
+ when(mMockContext.getSystemService(eq(UserManager.class)))
+ .thenReturn(mMockUserManager);
+ when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).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(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,6 +663,9 @@
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);
@@ -552,7 +676,7 @@
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 +704,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -609,7 +733,7 @@
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),
@@ -663,7 +787,7 @@
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -695,7 +819,7 @@
bindIntentCaptor2.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
bindIntent = bindIntentCaptor2.getValue();
assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
@@ -710,6 +834,7 @@
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCall.isIncoming()).thenReturn(false);
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 +866,9 @@
// 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 +877,259 @@
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(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.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 +1160,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
@@ -805,6 +1180,7 @@
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 +1199,7 @@
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 +1247,7 @@
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 +1275,7 @@
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 +1318,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind
assertEquals(2, bindIntentCaptor.getAllValues().size());
@@ -986,7 +1362,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind
assertEquals(1, bindIntentCaptor.getAllValues().size());
@@ -995,8 +1371,10 @@
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 +1397,7 @@
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 +1435,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
}
/**
@@ -1088,7 +1466,7 @@
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
// Verify bind car mode ui
assertEquals(4, bindIntentCaptor.getAllValues().size());
@@ -1125,6 +1503,7 @@
when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
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 +1524,7 @@
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(serviceBindingFlags),
- eq(UserHandle.CURRENT));
+ eq(mUserHandle));
CompletableFuture<Boolean> bindTimeout = mInCallController.getBindingFuture();
@@ -1213,7 +1592,7 @@
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 +1604,7 @@
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 +1630,7 @@
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 +1647,7 @@
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 +1663,79 @@
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));
+ }
+
private void setupMocks(boolean isExternalCall) {
setupMocks(isExternalCall, false /* isSelfManagedCall */);
}
@@ -1296,7 +1748,7 @@
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 +1830,14 @@
}};
}
- 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 +1928,7 @@
} 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 +1939,7 @@
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 d114cb8..88b5bb5 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.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 @@
}
@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 eadda0d..f11afc1 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 @@
@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 a871b73..a38de94 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 @@
when(mAudioCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
when(mAudioCall.getTargetPhoneAccountLabel()).thenReturn("Bar");
+ when(mAudioCall.getUserHandleFromTargetPhoneAccount()).
+ thenReturn(UserHandle.CURRENT);
when(mVideoCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
when(mVideoCall.getTargetPhoneAccountLabel()).thenReturn("Bar");
when(mRingingCall.isSelfManaged()).thenReturn(true);
@@ -79,6 +84,8 @@
when(mRingingCall.getState()).thenReturn(CallState.RINGING);
when(mRingingCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
when(mRingingCall.getTargetPhoneAccountLabel()).thenReturn("Foo");
+ when(mRingingCall.getUserHandleFromTargetPhoneAccount()).
+ thenReturn(UserHandle.CURRENT);
when(mRingingCall.getHandoverState()).thenReturn(HandoverState.HANDOVER_NONE);
}
@@ -95,8 +102,10 @@
@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 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 @@
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 @@
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 @@
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 @@
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 0000000..e441835
--- /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 f2f0cd8..8ea2739 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 @@
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 void testEmergencyCallPlacing() throws Exception {
Analytics.dumpToParcelableAnalytics();
setUpEmergencyCall();
+ when(mEmergencyCall.getUserHandleFromTargetPhoneAccount()).
+ thenReturn(mPhoneAccountA0.getAccountHandle().getUserHandle());
mCallsManager.addCall(mEmergencyCall);
assertTrue(mCallsManager.isInEmergencyCall());
@@ -354,6 +359,9 @@
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 0000000..ed74637
--- /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 2614abf..f2bcf18 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 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,6 +94,7 @@
@Mock private RoleManagerAdapter mRoleManagerAdapter;
@Mock private DefaultDialerCache mDefaultDialerCache;
+ @Mock private MmiUtils mMmiUtils;
private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl();
@Override
@@ -232,7 +234,7 @@
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 @@
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 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 @@
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 @@
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 @@
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 a503283..fed8084 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 @@
@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 @@
@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 @@
@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 @@
@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 ffa08e2..e573bb8 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.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.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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 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 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 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 @@
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 @@
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 @@
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 @@
}
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 @@
*/
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/ProximitySensorManagerTest.java b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
index eafaa53..807b7cf 100644
--- a/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
@@ -31,7 +31,7 @@
import org.junit.runners.JUnit4;
import org.mockito.Mock;
-import java.util.ArrayList;
+import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -67,9 +67,7 @@
@SmallTest
@Test
public void testTurnOnProximityWithCallsActive() throws Exception {
- when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>(){{
- add(mCall);
- }});
+ when(mCallsManager.getCalls()).thenReturn(List.of(mCall));
when(mWakeLockAdapter.isHeld()).thenReturn(false);
mProximitySensorManager.turnOn();
@@ -80,7 +78,7 @@
@SmallTest
@Test
public void testTurnOnProximityWithNoCallsActive() throws Exception {
- when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>());
+ when(mCallsManager.getCalls()).thenReturn(List.of());
when(mWakeLockAdapter.isHeld()).thenReturn(false);
mProximitySensorManager.turnOn();
@@ -102,9 +100,7 @@
@SmallTest
@Test
public void testCallRemovedFromCallsManagerCallsActive() throws Exception {
- when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>(){{
- add(mCall);
- }});
+ when(mCallsManager.getCalls()).thenReturn(List.of(mCall));
when(mWakeLockAdapter.isHeld()).thenReturn(true);
mProximitySensorManager.onCallRemoved(mock(Call.class));
@@ -115,7 +111,7 @@
@SmallTest
@Test
public void testCallRemovedFromCallsManagerNoCallsActive() throws Exception {
- when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>());
+ when(mCallsManager.getCalls()).thenReturn(List.of());
when(mWakeLockAdapter.isHeld()).thenReturn(true);
mProximitySensorManager.onCallRemoved(mock(Call.class));
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 12420a8..abbfe34 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.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.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.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 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.getUserHandleFromTargetPhoneAccount()).thenReturn(PA_HANDLE.getUserHandle());
+ when(mockCall2.getUserHandleFromTargetPhoneAccount()).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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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,39 @@
@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));
+ }
+
+ @SmallTest
+ @Test
+ public void testVibrateButNoRingForSilentRingtoneWithoutAudioHapticSupport() throws Exception {
+ mIsHapticPlaybackSupported = false;
+ createRingerUnderTest(); // Needed after changing haptic playback support.
+ mRingerUnderTest.startCallWaiting(mockCall1);
+ when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+ when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+ enableVibrationWhenRinging();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+ verify(mockTonePlayer).stopTone();
+ verifyZeroInteractions(mockRingtoneFactory);
+
// Play default vibration when future completes with no audio coupled haptics
verify(mockVibrator).vibrate(eq(mRingerUnderTest.mDefaultVibrationEffect),
any(VibrationAttributes.class));
@@ -326,19 +330,20 @@
@SmallTest
@Test
public void testAudioCoupledHapticsForSilentRingtone() throws Exception {
+ Ringtone mockRingtone = ensureRingtoneMocked();
+
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
+ setIsUsingHaptics(mockRingtone, true);
enableVibrationWhenRinging();
- assertFalse(mRingerUnderTest.startRinging(mockCall2, false));
- mRingCompletionFuture.get();
+ assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+
+ verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
+ verifyNoMoreInteractions(mockRingtoneFactory);
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(mockRingtone).play();
// Skip vibration for audio coupled haptics
verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
any(VibrationAttributes.class));
@@ -346,60 +351,36 @@
@SmallTest
@Test
- public void testStopRingingBeforeHapticsLookupComplete() throws Exception {
- enableVibrationWhenRinging();
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(nullable(Call.class))).thenReturn(mockRingtone);
- when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
-
- 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));
- }
-
- @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,52 +388,31 @@
@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());
- verify(mockVibrator, never())
- .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
- }
-
- @SmallTest
- @Test
- public void testSilentRingWithHfpStillAcquiresFocus2() 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();
- 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));
}
@@ -461,27 +421,156 @@
@Test
public void testRingAndVibrateForAllowedCallInDndMode() throws Exception {
mRingerUnderTest.startCallWaiting(mockCall1);
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtoneFactory.getRingtone(any(Call.class))).thenReturn(mockRingtone);
+ 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);
- mFuture.complete(true); // using audio coupled haptics
enableVibrationWhenRinging();
- assertTrue(mRingerUnderTest.startRinging(mockCall2, true));
- mRingCompletionFuture.get();
+ assertTrue(startRingingAndWaitForAsync(mockCall2, true));
+ verify(mockRingtoneFactory, times(1))
+ .getRingtone(any(Call.class), isNull(), anyBoolean());
+ verifyNoMoreInteractions(mockRingtoneFactory);
verify(mockTonePlayer).stopTone();
- verify(mockRingtonePlayer).play(any(RingtoneFactory.class), any(Call.class), eq(null),
- eq(true) /* isRingerAudible */, eq(true) /* isVibrationEnabled */);
+ verify(mockRingtone).play();
+ }
+
+ /**
+ * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with suppress
+ * caller
+ *
+ * @throws Exception; should not throw exception.
+ */
+ @Test
+ public void testShouldRingForContact_CallSuppressed() throws Exception {
+ // WHEN
+ when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
+ when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
+
+ when(mContext.getSystemService(NotificationManager.class)).thenReturn(
+ mockNotificationManager);
+ when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+
+ // THEN
+ assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
+ verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(true);
+ }
+
+ /**
+ * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with ring
+ * caller
+ *
+ * @throws Exception; should not throw exception.
+ */
+ @Test
+ public void testShouldRingForContact_CallShouldRing() throws Exception {
+ // WHEN
+ when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
+ when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
+ when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
+
+ // THEN
+ assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
+ verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(false);
+ }
+
+ @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), 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();
+ 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();
+ 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 +584,21 @@
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/SessionTest.java b/tests/src/com/android/server/telecom/tests/SessionTest.java
index 4be3dad..f38618c 100644
--- a/tests/src/com/android/server/telecom/tests/SessionTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionTest.java
@@ -144,6 +144,7 @@
* Ensure creating two sessions that are parent/child of each other does not lead to a crash
* or infinite recursion when using Session#toString.
*/
+ @SuppressWarnings("ReturnValueIgnored")
@SmallTest
@Test
public void testRecursion_toString() {
@@ -159,7 +160,6 @@
// Make sure calling these methods does not result in a crash
try {
-
parentSession.toString();
childSession.toString();
} catch (Exception e) {
@@ -176,6 +176,7 @@
* Ensure creating two sessions and setting the child as the parent to itself doesn't cause a
* crash due to infinite recursion.
*/
+ @SuppressWarnings("ReturnValueIgnored")
@SmallTest
@Test
public void testRecursion_toString_childCircDep() {
@@ -237,6 +238,7 @@
* Ensure creating two sessions that are parent/child of each other does not lead to a crash
* or infinite recursion in the general case.
*/
+ @SuppressWarnings("ReturnValueIgnored")
@SmallTest
@Test
public void testRecursion() {
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 90bdc80..7b5afe6 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 @@
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.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.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.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.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 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.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 @@
@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 @@
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 @@
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 @@
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,40 +421,106 @@
.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
@Test
public void testGetCallCapablePhoneAccounts() throws RemoteException {
- List<PhoneAccountHandle> fullPHList = new ArrayList<PhoneAccountHandle>() {{
- add(TEL_PA_HANDLE_16);
- add(SIP_PA_HANDLE_17);
- }};
+ List<PhoneAccountHandle> fullPHList = List.of(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+ List<PhoneAccountHandle> smallPHList = List.of(SIP_PA_HANDLE_17);
- List<PhoneAccountHandle> smallPHList = new ArrayList<PhoneAccountHandle>() {{
- add(SIP_PA_HANDLE_17);
- }};
// 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,
@@ -400,40 +533,78 @@
@SmallTest
@Test
- public void testGetCallCapablePhoneAccountsFailure() throws RemoteException {
- List<String> enforcedPermissions = new ArrayList<String>() {{
- add(READ_PHONE_STATE);
- add(READ_PRIVILEGED_PHONE_STATE);
- }};
+ 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
@Test
public void testGetPhoneAccountsSupportingScheme() throws RemoteException {
- List<PhoneAccountHandle> sipPHList = new ArrayList<PhoneAccountHandle>() {{
- add(SIP_PA_HANDLE_17);
- }};
+ List<PhoneAccountHandle> sipPHList = List.of(SIP_PA_HANDLE_17);
+ List<PhoneAccountHandle> telPHList = List.of(TEL_PA_HANDLE_16);
- List<PhoneAccountHandle> telPHList = new ArrayList<PhoneAccountHandle>() {{
- add(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);
@@ -447,13 +618,21 @@
@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 = new ArrayList<PhoneAccountHandle>() {{
- add(TEL_PA_HANDLE_16);
- add(SIP_PA_HANDLE_17);
- }};
+ 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,
@@ -463,7 +642,20 @@
@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());
@@ -479,15 +671,96 @@
@SmallTest
@Test
- public void testGetAllPhoneAccounts() throws RemoteException {
- List<PhoneAccount> phoneAccountList = new ArrayList<PhoneAccount>() {{
- add(makePhoneAccount(TEL_PA_HANDLE_16).build());
- add(makePhoneAccount(SIP_PA_HANDLE_17).build());
- }};
- when(mFakePhoneAccountRegistrar.getAllPhoneAccounts(any(UserHandle.class)))
+ 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(2, mTSIBinder.getAllPhoneAccounts().getList().size());
+ 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), anyBoolean()))
+ .thenReturn(phoneAccountList);
+
+ 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
@@ -505,6 +778,65 @@
@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
@@ -602,7 +934,7 @@
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
boolean didExceptionOccur = false;
try {
- mTSIBinder.registerPhoneAccount(testPhoneAccount);
+ mTSIBinder.registerPhoneAccount(testPhoneAccount, CALLING_PACKAGE);
} catch (Exception e) {
didExceptionOccur = true;
}
@@ -619,6 +951,26 @@
@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(
@@ -628,7 +980,7 @@
doReturn(PackageManager.PERMISSION_GRANTED)
.when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
- mTSIBinder.unregisterPhoneAccount(phHandle);
+ mTSIBinder.unregisterPhoneAccount(phHandle, CALLING_PACKAGE);
verify(mFakePhoneAccountRegistrar).unregisterPhoneAccount(phHandle);
}
@@ -645,7 +997,7 @@
when(pm.hasSystemFeature(PackageManager.FEATURE_TELECOM)).thenReturn(false);
try {
- mTSIBinder.unregisterPhoneAccount(phHandle);
+ mTSIBinder.unregisterPhoneAccount(phHandle, CALLING_PACKAGE);
} catch (UnsupportedOperationException e) {
// expected behavior
}
@@ -657,25 +1009,47 @@
@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
}
@@ -683,7 +1057,7 @@
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
}
@@ -706,7 +1080,7 @@
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
@@ -732,7 +1106,8 @@
}
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),
@@ -744,7 +1119,7 @@
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));
@@ -760,12 +1135,18 @@
}
}
+ /**
+ * 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);
@@ -775,15 +1156,339 @@
.when(mContext).checkCallingPermission(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).checkCallingPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingPermission(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());
+ 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());
+ 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).checkCallingPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingPermission(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());
+ 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());
+ 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).checkCallingPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingPermission(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).checkCallingPermission(CALL_PHONE);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingPermission(CALL_PRIVILEGED);
+
+ mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE, null);
+ placeCallTestHelper(handle, extras, /*isSelfManagedExpected*/ false,
+ /*shouldNonEmergencyBeAllowed*/ true);
+ }
+
+ /**
+ * 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);
@@ -793,15 +1498,21 @@
.when(mContext).checkCallingPermission(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);
@@ -811,37 +1522,82 @@
.when(mContext).checkCallingPermission(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());
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));
+ }
+
+ /**
+ * 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());
+ 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(), eq(false), anyBoolean(), eq(true));
}
@SmallTest
@@ -1103,6 +1859,29 @@
@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);
@@ -1136,8 +1915,7 @@
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));
}
@@ -1178,14 +1956,16 @@
@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));
}
@@ -1200,20 +1980,22 @@
} 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));
}
@@ -1228,7 +2010,7 @@
} catch (SecurityException e) {
// desired result
}
- verify(mFakeCallsManager, never()).hasOngoingCalls();
+ verify(mFakeCallsManager, never()).hasOngoingCalls(any(UserHandle.class), anyBoolean());
}
/**
@@ -1264,6 +2046,22 @@
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.
@@ -1288,6 +2086,12 @@
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");
}
@@ -1299,6 +2103,8 @@
}
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 d6ff196..d013fae 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.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.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.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.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
@@ -119,6 +121,7 @@
*/
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 @@
@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 @@
@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 @@
// 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();
+ }
- mConnectionServiceFixtureA.waitForHandlerToClear();
- mConnectionServiceFixtureB.waitForHandlerToClear();
+ if (mConnectionServiceFixtureA != null) {
+ mConnectionServiceFixtureA.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.
@@ -473,7 +490,8 @@
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 +516,8 @@
WiredHeadsetManager wiredHeadsetManager,
StatusBarNotifier statusBarNotifier,
CallAudioManager.AudioServiceFactory audioServiceFactory,
- int earpieceControl) {
+ int earpieceControl,
+ Executor asyncTaskExecutor) {
return new CallAudioRouteStateMachine(context,
callsManager,
bluetoothManager,
@@ -507,7 +526,8 @@
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 +546,9 @@
ContactsAsyncHelper.ContentResolverAdapter adapter) {
return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
}
- }, mDeviceIdleControllerAdapter);
+ }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
+ Runnable::run,
+ mBlockedNumbersAdapter);
mComponentContextFixture.setTelecomManager(new TelecomManager(
mComponentContextFixture.getTestDouble(),
@@ -743,7 +765,7 @@
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.
@@ -904,7 +926,7 @@
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(),
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 0000000..22a0be1
--- /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 0000000..3fc87a9
--- /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 0000000..98624d4
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.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;
+ 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);
+
+ 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/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
new file mode 100644
index 0000000..346b3d8
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.telecom.PhoneAccountHandle;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+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 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";
+ 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 NotificationListenerService mListenerService;
+
+ 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);
+ mListenerService = mock(NotificationListenerService.class);
+ mMonitor.setActivityManagerInternal(mActivityManagerInternal);
+ mMonitor.setNotificationListenerService(mListenerService);
+ doNothing().when(mListenerService).registerAsSystemService(eq(mContext),
+ any(ComponentName.class), anyInt());
+ mMonitor.startMonitor();
+ }
+
+ @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(mHandle1User1);
+ 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));
+ }
+
+ 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() );
+ return call;
+ }
+}
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 0000000..e2c7b7b
--- /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(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"));
+ }
+}