Merge "Remove code which prevents making a video call when one is in progress." into mm-wireless-dev
am: f21d66d3d8
* commit 'f21d66d3d8d57440e0e3110a581f04f8ba3d7f4e':
Remove code which prevents making a video call when one is in progress.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 56a594b..887c2e7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,7 +20,6 @@
coreApp="true"
android:sharedUserId="android.uid.system">
-
<protected-broadcast android:name="android.intent.action.SHOW_MISSED_CALLS_NOTIFICATION" />
<!-- Prevents the activity manager from delaying any activity-start
@@ -63,16 +62,14 @@
android:label="Process phone account registration"
android:protectionLevel="signature|system"/>
- <!-- Declare which SDK level this application was built against. This is needed so that IDEs
- can check for incompatible APIs. -->
- <uses-sdk android:minSdkVersion="19" />
-
<application android:label="@string/telecommAppLabel"
android:icon="@mipmap/ic_launcher_phone"
android:allowBackup="false"
android:supportsRtl="true"
android:process="system"
- android:usesCleartextTraffic="false">
+ android:usesCleartextTraffic="false"
+ android:forceDeviceEncrypted="true"
+ android:encryptionAware="true">
<!-- CALL vs CALL_PRIVILEGED vs CALL_EMERGENCY
We have three different intents through which a call can be initiated each with its
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index b9aa6b9..3984a5d 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Foon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Foonoproepbestuur"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Bel"</string>
<string name="unknown" msgid="6878797917991465859">"Onbekend"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Gemiste oproep"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Gemiste werkoproep"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Gemiste oproepe"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> gemiste oproepe"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Gemiste oproep van <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Vinnige antwoord"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Boodskap gestuur na <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Net noodoproepe word deur die toesteleienaar toegelaat"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Oproeprekeninge"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Net noodoproepe word toegelaat."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Hierdie program kan nie uitgaande oproepe maak sonder die foon se toestemming nie."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Voer \'n geldige nommer in om \'n oproep te maak."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Oproep kan nie op die oomblik bygevoeg word nie."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Deaktiveer TTY-modus om video-oproepe te maak."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Vermiste stemboodskapnommer"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Geen stemboodskapnommer is op die SIM-kaart gestoor nie."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Voeg nommer by"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Verander verstekbellerprogram?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Gebruik <xliff:g id="NEW_APP">%1$s</xliff:g> eerder as <xliff:g id="CURRENT_APP">%2$s</xliff:g> as jou verstekbellerprogram?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Gebruik <xliff:g id="NEW_APP">%s</xliff:g> as jou verstekbellerprogram?"</string>
</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 758b795..dbd4c30 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ስልክ"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"የስልክ ጥሪ አስተዳደር"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ስልክ"</string>
<string name="unknown" msgid="6878797917991465859">"ያልታወቀ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"ያመለጠጥሪ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"ያመለጠ የሥራ ጥሪ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"ያመለጡ ጥሪዎች"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ያመለጡ ጥሪዎች"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"ከ<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> ያመለጠ ጥሪ"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ፈጣን ምላሽ"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"ለ <xliff:g id="PHONE_NUMBER">%s</xliff:g> የተላከ መልዕክት"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"የአስቸኳይ አደጋ ጥሪዎች ብቻ ናቸው በባለቤቱ የተፈቀዱት"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"የመደወያ መለያዎች"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"የድንገተኛ አደጋ ጥሪዎች ብቻ ናቸው የሚፈቀዱት።"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ይህ መተግበሪያ ያለስልኩ ፈቃድ ወጪ ጥሪዎችን ማድረግ አይችልም።"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"አንድ ጥሪ ለማድረግ የሚሰራ ቁጥር ያስገቡ።"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ጥሪ በዚህ ጊዜ ላይ ሊታከል አይችልም።"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"የቪዲዮ ጥሪዎችን ለማድረግ እባክዎ የTTY ሁነታን ያሰናክሉ።"</string>
<string name="no_vm_number" msgid="4164780423805688336">"የድምፅመልዕክት ቁጥርአመለጠ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"በSIM ካርዱ ላይምንም የድምፅመልዕክት ቁጥር አልከተቀመጠም።"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"ቁጥር አክል"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ነባሪ መደወያ መተግበሪያ ይለወጥ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g>ን በ<xliff:g id="CURRENT_APP">%2$s</xliff:g> ምትክ እንደ የእርስዎ ነባሪ መደወያ መተግበሪያ ይጠቀም?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g>ን እንደ የእርስዎ ነባሪ መደወያ መተግበሪያ ይጠቀም?"</string>
</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 3d31dc6..0ea39f5 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"الهاتف"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"إدارة المكالمات الهاتفية"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"الهاتف"</string>
<string name="unknown" msgid="6878797917991465859">"غير معروف"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"مكالمة فائتة"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"مكالمة عمل فائتة"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"المكالمات الفائتة"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> من المكالمات الفائتة"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"مكالمة فائتة من <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"رد سريع"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"تم إرسال الرسالة إلى <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"مسموح لمالك الجهاز بمكالمات الطوارئ فقط"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"حسابات الاتصال"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"مسموح بمكالمات الطوارئ فقط."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"يتعذر على هذا التطبيق إجراء مكالمات صادرة بدون إذن من الهاتف."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"لإجراء مكالمة، أدخل رقمًا صالحًا."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"لا يمكن إضافة مكالمة في الوقت الحالي."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"يُرجى تعطيل وضع TTY لإجراء مكالمات فيديو."</string>
<string name="no_vm_number" msgid="4164780423805688336">"رقم البريد الصوتي مفقود"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"لم يتم تخزين رقم بريد صوتي على شريحة SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"إضافة رقم"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"هل تريد تغيير التطبيق الافتراضي لبرنامج الاتصال؟"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"هل تريد استخدام <xliff:g id="NEW_APP">%1$s</xliff:g> بدلاً من <xliff:g id="CURRENT_APP">%2$s</xliff:g> تطبيقًا افتراضيًا لبرنامج الاتصال؟"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"هل تريد استخدام <xliff:g id="NEW_APP">%s</xliff:g> تطبيقًا افتراضيًا لبرنامج الاتصال؟"</string>
</resources>
diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml
new file mode 100644
index 0000000..353de80
--- /dev/null
+++ b/res/values-az-rAZ/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefon Zənglərin İdarə Olunması"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
+ <string name="unknown" msgid="6878797917991465859">"Naməlum"</string>
+ <string name="notification_missedCallTitle" msgid="7554385905572364535">"Buraxılmış zəng"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Buraxılmış iş çağrısı"</string>
+ <string name="notification_missedCallsTitle" msgid="1361677948941502522">"Buraxılmış zənglər"</string>
+ <string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> buraxılmış zənglər"</string>
+ <string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> tərəfindən zəng buraxılıb"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Geriyə zəng"</string>
+ <string name="notification_missedCall_message" msgid="3049928912736917988">"Mesaj"</string>
+ <string name="accessibility_call_muted" msgid="2776111226185342220">"Səssiz zəng edin."</string>
+ <string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Spikerfon aktivdir."</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"İndi danışmaq olmur. Nə olub?"</string>
+ <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Özüm zəng edəcəm."</string>
+ <string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Özüm sonra zəng edəcəm."</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"İndi danışa bilmirəm. Sonra zəng edin."</string>
+ <string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Tez cavablar"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Tez cavablara düzəliş edin"</string>
+ <string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
+ <string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Tez cavab"</string>
+ <string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mesaj <xliff:g id="PHONE_NUMBER">%s</xliff:g> nömrəsinə göndərildi."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Hesabların çağırılması"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Yalnız təcili zənglərə icazə verilir."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Bu proqram Telefon icazəsi olmadan zəng edə bilməz."</string>
+ <string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Zəngi yerləşdirmək üçün düzgün nömrə daxil edin."</string>
+ <string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Hazırda çağrı edilə bilməz."</string>
+ <string name="no_vm_number" msgid="4164780423805688336">"Səsli poçt nömrəsi çatışmır"</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"SIM kartda heç bir səsli poçt nömrəsi yoxdur."</string>
+ <string name="add_vm_number_str" msgid="4676479471644687453">"Nömrə əlavə edin"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Defolt nömrə yığan tətbiqi dəyişdirilsin?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Defolt nömrə yığan tətbiqi kimi <xliff:g id="CURRENT_APP">%2$s</xliff:g> əvəzinə <xliff:g id="NEW_APP">%1$s</xliff:g> işlədilsin?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Defolt nömrə yığan tətbiqi kimi <xliff:g id="NEW_APP">%s</xliff:g> işlədilsin?"</string>
+</resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..057b025
--- /dev/null
+++ b/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="telecommAppLabel" product="default" msgid="9166784827254469057">"Upravljanje telefonskim pozivima"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
+ <string name="unknown" msgid="6878797917991465859">"Nepoznato"</string>
+ <string name="notification_missedCallTitle" msgid="7554385905572364535">"Propušten poziv"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Propušten poziv za Work"</string>
+ <string name="notification_missedCallsTitle" msgid="1361677948941502522">"Propušteni pozivi"</string>
+ <string name="notification_missedCallsMsg" msgid="4575787816055205600">"Broj propuštenih poziva: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>"</string>
+ <string name="notification_missedCallTicker" msgid="504686252427747209">"Propušten poziv od: <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Uzvrati poziv"</string>
+ <string name="notification_missedCall_message" msgid="3049928912736917988">"Poruka"</string>
+ <string name="accessibility_call_muted" msgid="2776111226185342220">"Zvuk poziva je isključen."</string>
+ <string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Spikerfon je omogućen."</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"U gužvi sam. O čemu se radi?"</string>
+ <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Pozvaću te uskoro."</string>
+ <string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Pozvaću te kasnije."</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"U gužvi sam. Da se čujemo kasnije?"</string>
+ <string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Brzi odgovori"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Izmena brzih odgovora"</string>
+ <string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
+ <string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Brzi odgovor"</string>
+ <string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Poruka je poslata na broj <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Nalozi za pozivanje"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Dozvoljeni su samo hitni pozivi."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ova aplikacija ne može da poziva bez dozvole za telefoniranje."</string>
+ <string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Da biste uputili poziv, unesite važeći broj."</string>
+ <string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Trenutno nije moguće dodati poziv."</string>
+ <string name="no_vm_number" msgid="4164780423805688336">"Nedostaje broj za govornu poštu"</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"Nije uskladišten nijedan broj govorne pošte na SIM kartici."</string>
+ <string name="add_vm_number_str" msgid="4676479471644687453">"Dodaj broj"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Promeniti podrazumevanu aplikaciju Telefon?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Želite li da koristite aplikaciju <xliff:g id="NEW_APP">%1$s</xliff:g> umesto aplikacije <xliff:g id="CURRENT_APP">%2$s</xliff:g> kao podrazumevanu aplikaciju za pozivanje telefonskih brojeva?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Želite li da koristite aplikaciju <xliff:g id="NEW_APP">%s</xliff:g> kao podrazumevanu aplikaciju za pozivanje telefonskih brojeva?"</string>
+</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 63d32b1..fa451b7 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Телефон"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Управление на телефонните обаждания"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
<string name="unknown" msgid="6878797917991465859">"Неизвестен номер"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Пропуснато обаждане"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Пропуснато служебно обаждане"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Пропуснати обаждания"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> пропуснати обаждания"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Пропуснато обаждане от <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -31,15 +33,19 @@
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Ще ви се обадя по-късно."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Сега не мога да говоря. По-късно?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Бързи отговори"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Редактиране на бързи отговори"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Редакт. на бързи отговори"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Бърз отговор"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"До <xliff:g id="PHONE_NUMBER">%s</xliff:g> бе изпратено съобщение."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Собственикът на устройството разрешава само спешни обаждания"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Профили за обаждане"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Разрешени са само спешни обаждания."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Това приложение не може да извършва изходящи обаждания без разрешението за телефон."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"За да извършите обаждане, въведете валиден номер."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Понастоящем обаждането не може да бъде добавено."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Моля, деактивирайте режима на TTY, за да извършвате видеообаждания."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Липсващ номер на гласова поща"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"На SIM картата няма съхранен номер за гласова поща."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Добавяне на номер"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Да се промени ли основното приложение за дайлер?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g> да се използва ли вместо <xliff:g id="CURRENT_APP">%2$s</xliff:g> като основното ви приложение за дайлер?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Да се използва ли <xliff:g id="NEW_APP">%s</xliff:g> като основното ви приложение за дайлер?"</string>
</resources>
diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml
index 6922524..787c331 100644
--- a/res/values-bn-rBD/strings.xml
+++ b/res/values-bn-rBD/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ফোন"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ফোন কল ব্যবস্থাপনা"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ফোন"</string>
<string name="unknown" msgid="6878797917991465859">"অজানা"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"মিসড কল"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"কাজের কল মিস করেছেন"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"মিসড কলগুলি"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>টি মিসড কল"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> এর থেকে মিসড কল"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"দ্রুত প্রতিক্রিয়া"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> এ বার্তা পাঠানো হয়েছে৷"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ডিভাইসের মালিক শুধুমাত্র জরুরি কলগুলিতে অনুমতি দিয়েছেন"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"কলিং অ্যাকাউন্টগুলি"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"শুধুমাত্র জরুরি কলগুলিকে অনুমোদিত।"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"এই অ্যাপ্লিকেশানটি ফোনের অনুমতি ছাড়া আউটগোয়িং কলগুলি করতে পারবে না।"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"কোনো কল স্থাপন করতে, একটি বৈধ নম্বর লিখুন৷"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"এই মুহূর্তে কল যোগ করা যাবে না৷"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"অনুগ্রহ করে ভিডিও কলগুলি করতে TTY মোড অক্ষম করুন৷"</string>
<string name="no_vm_number" msgid="4164780423805688336">"ভয়েসমেল নম্বর অনুপস্থিত"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"সিম কার্ডটিতে কোনো ভয়েসমেল নম্বর সংরক্ষিত নেই৷"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"একটি নম্বর যোগ করুন"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ডিফল্ট ডায়ালার অ্যাপ পরিবর্তন করবেন?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"আপনার ডিফল্ট ডায়লার অ্যাপ হিসেবে <xliff:g id="CURRENT_APP">%2$s</xliff:g> পরিবর্তে <xliff:g id="NEW_APP">%1$s</xliff:g> ব্যবহার করবেন?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"আপনার ডিফল্ট ডায়লার অ্যাপ হিসেবে <xliff:g id="NEW_APP">%s</xliff:g> ব্যবহার করবেন?"</string>
</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index e3cf515..dc5b73c 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telèfon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Gestió de trucades telefòniques"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telèfon"</string>
<string name="unknown" msgid="6878797917991465859">"Desconegut"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Trucada perduda"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Trucada perduda de feina"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Trucades perdudes"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> trucades perdudes"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Trucada perduda de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Resposta ràpida"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Missatge enviat a <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"El propietari del dispositiu només permet les trucades d\'emergència."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Comptes de trucades"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Només es permeten les trucades d\'emergència."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Aquesta aplicació no pot fer trucades sortints sense el permís del telèfon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Per realitzar una trucada, introdueix un número vàlid."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"En aquest moment no es pot afegir la trucada."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Desactiva el mode de TTY per fer videotrucades."</string>
- <string name="no_vm_number" msgid="4164780423805688336">"Falta el número de correu de veu"</string>
- <string name="no_vm_number_msg" msgid="1300729501030053828">"No hi ha cap número de correu de veu emmagatzemat a la targeta SIM."</string>
+ <string name="no_vm_number" msgid="4164780423805688336">"Falta el número de la bústia de veu"</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"No hi ha cap número de bústia de veu emmagatzemat a la targeta SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Afegeix número"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Vols canviar l\'aplicació de marcador predeterminada?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Vols fer servir <xliff:g id="NEW_APP">%1$s</xliff:g> en lloc de <xliff:g id="CURRENT_APP">%2$s</xliff:g> com a aplicació de marcador predeterminada?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Vols fer servir <xliff:g id="NEW_APP">%s</xliff:g> com a aplicació de marcador predeterminada?"</string>
</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 259cca9..dbc7165 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Správa telefonních hovorů"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Neznámý volající"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Zmeškaný hovor"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Zmeškaný pracovní hovor"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Zmeškané hovory"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"Zmeškané hovory: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>."</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Zmeškaný hovor od volajícího <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>."</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Rychlá odpověď"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Zpráva byla odeslána na číslo <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Vlastník zařízení povolil pouze tísňová volání."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Účty pro volání"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Jsou povolena pouze tísňová volání."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Tato aplikace nemůže provádět odchozí hovory bez oprávnění k použití telefonu."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Chcete-li uskutečnit hovor, zadejte platné telefonní číslo."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Hovor aktuálně nelze přidat."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Chcete-li vést videohovory, vypněte režim TTY."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Chybí číslo hlasové schránky"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Na SIM kartě není uloženo žádné číslo hlasové schránky."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Přidat číslo"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Změnit výchozí aplikaci vytáčení?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Chcete aplikaci <xliff:g id="NEW_APP">%1$s</xliff:g> použít jako výchozí aplikaci vytáčení místo aplikace <xliff:g id="CURRENT_APP">%2$s</xliff:g>?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Chcete aplikaci <xliff:g id="NEW_APP">%s</xliff:g> použít jako výchozí aplikaci vytáčení?"</string>
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 83be7ed..8b1f7df 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Opkaldsstyring"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Opkald"</string>
<string name="unknown" msgid="6878797917991465859">"Ukendt"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Ubesvarede opkald"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Ubesvaret arbejdsopkald"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Ubesvarede opkald"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ubesvarede opkald"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Ubesvarede opkald fra <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Hurtigt svar"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Meddelelsen er sendt til <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Enhedens ejer tillader kun nødopkald."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Opkaldskonti"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Kun nødopkald er tilladt."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Denne app kan ikke foretage udgående opkald uden opkaldstilladelse."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Indtast et gyldigt nummer for at foretage et opkald."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Opkaldet kan ikke tilføjes på nuværende tidspunkt."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Deaktiver TTY-tilstanden for at foretage videoopkald."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Telefonsvarernummer mangler"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Der er ikke gemt noget telefonsvarernummer på SIM-kortet."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Tilføj nummer"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Vil du skifte standardappen til opkald?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Brug <xliff:g id="NEW_APP">%1$s</xliff:g> i stedet for <xliff:g id="CURRENT_APP">%2$s</xliff:g> som din standardapp til opkald?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Brug <xliff:g id="NEW_APP">%s</xliff:g> som din standardapp til opkald?"</string>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 29aecb2..44277c8 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Anrufverwaltung"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Unbekannt"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Entgangener Anruf"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Verpasster geschäftlicher Anruf"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Entgangene Anrufe"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> entgangene Anrufe"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Entgangener Anruf von <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Kurzantwort"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Nachricht an <xliff:g id="PHONE_NUMBER">%s</xliff:g> gesendet"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Der Geräteinhaber hat nur Notrufe zugelassen."</string>
- <string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Geben Sie eine gültige Nummer ein."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Anrufkonten"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Es sind nur Notrufe erlaubt."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Diese App darf ohne die Berechtigung \"Standard-App für Telefonie\" keine ausgehenden Anrufe tätigen."</string>
+ <string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Gib eine gültige Nummer ein."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Der Anruf kann momentan nicht hinzugefügt werden."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Bitte deaktivieren Sie den TTY-Modus, um Videoanrufe tätigen zu können."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Fehlende Mailbox-Nummer"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Auf der SIM-Karte ist keine Mailbox-Nummer gespeichert."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Nummer hinzufügen"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Standard-App für Telefonie ändern?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g> statt <xliff:g id="CURRENT_APP">%2$s</xliff:g> als Standard-App für Telefonie verwenden?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> als Standard-App für Telefonie verwenden?"</string>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index a4249bb..af41ee4 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Τηλέφωνο"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Διαχείριση τηλεφωνικών κλήσεων"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Τηλέφωνο"</string>
<string name="unknown" msgid="6878797917991465859">"Άγνωστος"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Αναπάντητη κλήση"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Αναπάντητη κλήση εργασίας"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Αναπάντητες κλήσεις"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> αναπάντητες κλήσεις"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Αναπάντητη κλήση από <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -31,15 +33,19 @@
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Θα σου τηλεφωνήσω αργότερα."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Δεν μπορώ τώρα. Πάρε με αργότερα."</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Γρήγορες απαντήσεις"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Επεξεργασία γρήγορων απαντήσεων"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Επεξεργασία"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Γρήγορη απάντηση"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Το μήνυμα εστάλη στο <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Από τον κάτοχο της συσκευής επιτρέπονται μόνο κλήσεις έκτακτης ανάγκης"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Λογαριασμοί κλήσης"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Επιτρέπονται μόνο κλήσεις έκτακτης ανάγκης."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Αυτή η εφαρμογή δεν μπορεί να πραγματοποιήσει εξερχόμενες κλήσεις χωρίς την άδεια \"Τηλέφωνο\"."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Για να πραγματοποιήσετε μια κλήση, εισαγάγετε έναν έγκυρο αριθμό."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Δεν είναι δυνατή η προσθήκη κλήσης αυτήν τη στιγμή."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Απενεργοποιήστε τη λειτουργία TTY για να πραγματοποιήσετε βιντεοκλήσεις."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Λείπει ο αριθμός αυτόματου τηλεφωνητή"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Δεν έχει αποθηκευτεί αριθμός για τον αυτόματο τηλεφωνητή στην κάρτα SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Προσθήκη αριθμού"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Αλλαγή της προεπιλεγμένης εφαρμογής Dialer;"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Χρήση της εφαρμογής <xliff:g id="NEW_APP">%1$s</xliff:g> αντί για την εφαρμογή <xliff:g id="CURRENT_APP">%2$s</xliff:g> ως προεπιλεγμένης εφαρμογής Dialer;"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Χρήση της εφαρμογής <xliff:g id="NEW_APP">%s</xliff:g> ως προεπιλεγμένης εφαρμογής Dialer;"</string>
</resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 97d4c9c..a7d9e24 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Phone"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Phone Call Management"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telephone"</string>
<string name="unknown" msgid="6878797917991465859">"Unknown"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Missed call"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Missed work call"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Missed calls"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> missed calls"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Missed call from <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Quick response"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Message sent to <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Only emergency calls are allowed by the device owner"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Calling accounts"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Only emergency calls are allowed."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"This application cannot make outgoing calls without Phone permission."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"To place a call, enter a valid number."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Call cannot be added at this time."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Please disable TTY Mode to make video calls."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Missing voicemail number"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"No voicemail number is stored on the SIM card."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Add number"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Change default Dialer app?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Use <xliff:g id="NEW_APP">%1$s</xliff:g> instead of <xliff:g id="CURRENT_APP">%2$s</xliff:g> as your default dialler app?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Use <xliff:g id="NEW_APP">%s</xliff:g> as your default dialler app?"</string>
</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 97d4c9c..a7d9e24 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Phone"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Phone Call Management"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telephone"</string>
<string name="unknown" msgid="6878797917991465859">"Unknown"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Missed call"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Missed work call"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Missed calls"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> missed calls"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Missed call from <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Quick response"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Message sent to <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Only emergency calls are allowed by the device owner"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Calling accounts"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Only emergency calls are allowed."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"This application cannot make outgoing calls without Phone permission."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"To place a call, enter a valid number."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Call cannot be added at this time."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Please disable TTY Mode to make video calls."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Missing voicemail number"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"No voicemail number is stored on the SIM card."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Add number"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Change default Dialer app?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Use <xliff:g id="NEW_APP">%1$s</xliff:g> instead of <xliff:g id="CURRENT_APP">%2$s</xliff:g> as your default dialler app?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Use <xliff:g id="NEW_APP">%s</xliff:g> as your default dialler app?"</string>
</resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 97d4c9c..a7d9e24 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Phone"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Phone Call Management"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telephone"</string>
<string name="unknown" msgid="6878797917991465859">"Unknown"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Missed call"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Missed work call"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Missed calls"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> missed calls"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Missed call from <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Quick response"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Message sent to <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Only emergency calls are allowed by the device owner"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Calling accounts"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Only emergency calls are allowed."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"This application cannot make outgoing calls without Phone permission."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"To place a call, enter a valid number."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Call cannot be added at this time."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Please disable TTY Mode to make video calls."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Missing voicemail number"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"No voicemail number is stored on the SIM card."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Add number"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Change default Dialer app?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Use <xliff:g id="NEW_APP">%1$s</xliff:g> instead of <xliff:g id="CURRENT_APP">%2$s</xliff:g> as your default dialler app?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Use <xliff:g id="NEW_APP">%s</xliff:g> as your default dialler app?"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 3b6bd73..5d6e049 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -16,17 +16,19 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Teléfono"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Administración de llamadas telefónicas"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Teléfono"</string>
<string name="unknown" msgid="6878797917991465859">"Desconocida"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Llamada perdida"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Llamada de trabajo perdida"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Llamadas perdidas"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> llamadas perdidas"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Se perdieron las llamadas de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Devolver llamada"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Llamar"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Mensaje"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Llamada silenciada"</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Altavoz habilitado"</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"No puedo hablar ahora. ¿Qué pasa?"</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"No puedo hablar ahora. ¿Todo bien?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Te llamo enseguida."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Te llamo más tarde."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"No puedo hablar ahora. ¿Me llamas más tarde?"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Respuesta rápida"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mensaje enviado a <xliff:g id="PHONE_NUMBER">%s</xliff:g>"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"El propietario del dispositivo solo permite las llamadas de emergencia."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Cuentas telefónicas"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Solo se permiten llamadas de emergencia."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Esta aplicación no puede realizar llamadas salientes sin permiso del teléfono."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Para realizar una llamada, ingresa un número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"No se puede agregar la llamada en este momento."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Inhabilita el modo TTY para realizar una videollamada."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Falta el número de correo de voz"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"No hay un número de correo de voz almacenado en la tarjeta SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Agregar número"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"¿Quieres cambiar la aplicación Marcador predeterminada?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"¿Quieres usar <xliff:g id="NEW_APP">%1$s</xliff:g> en lugar de <xliff:g id="CURRENT_APP">%2$s</xliff:g> como la aplicación de marcado predeterminada?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"¿Quieres usar <xliff:g id="NEW_APP">%s</xliff:g> como la aplicación de marcado predeterminada?"</string>
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 7287095..ab4a174 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Teléfono"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Gestión de llamadas del teléfono"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Teléfono"</string>
<string name="unknown" msgid="6878797917991465859">"Desconocido"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Llamada perdida"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Llamada de trabajo perdida"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Llamadas perdidas"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> llamadas perdidas"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Llamada perdida de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Respuestas rápidas"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mensaje enviado a <xliff:g id="PHONE_NUMBER">%s</xliff:g>"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"El propietario del dispositivo solo permite llamadas de emergencia"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Cuentas de llamadas"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Solo se permiten llamadas de emergencia."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Esta aplicación no puede hacer llamadas sin permiso del teléfono."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Para realizar una llamada, introduce un número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"No se puede añadir la llamada en este momento."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Inhabilita el modo TTY para hacer videollamadas."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Falta el número del buzón de voz."</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"No se ha almacenado ningún número de buzón de voz en la tarjeta SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Añadir número"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"¿Cambiar el marcador predeterminado?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"¿Quieres utilizar <xliff:g id="NEW_APP">%1$s</xliff:g> en lugar de <xliff:g id="CURRENT_APP">%2$s</xliff:g> como marcador predeterminado?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"¿Quieres utilizar <xliff:g id="NEW_APP">%s</xliff:g> como marcador predeterminado?"</string>
</resources>
diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml
index 47627f8..a567faf 100644
--- a/res/values-et-rEE/strings.xml
+++ b/res/values-et-rEE/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefonikõnede haldus"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Tundmatu"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Vastamata kõne"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Vastamata kõne töölt"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Vastamata kõned"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> vastamata kõnet"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Vastamata kõne helistajalt <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Kiirvastus"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Sõnum on saadetud numbrile <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Seadme omanik lubab ainult hädaabikõnesid"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Kõnekontod"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Lubatud on ainult hädaabikõned."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"See rakendus ei saa ilma telefoni kasutamise loata välja helistada."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Helistamiseks sisestage kehtiv number."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Kõnet ei saa praegu lisada."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Videokõnede tegemiseks keelake TTY-režiim."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Puudub kõnepostinumber"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM-kaardile pole salvestatud ühtegi kõnepostinumbrit."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Lisa number"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Kas muuta helistamise vaikerakendust?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Kas kasutada rakendust <xliff:g id="NEW_APP">%1$s</xliff:g> rakenduse <xliff:g id="CURRENT_APP">%2$s</xliff:g> asemel helistamise vaikerakendusena?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Kas kasutada rakendust <xliff:g id="NEW_APP">%s</xliff:g> helistamise vaikerakendusena?"</string>
</resources>
diff --git a/res/values-eu-rES/strings.xml b/res/values-eu-rES/strings.xml
index e35b3fe..3cf0454 100644
--- a/res/values-eu-rES/strings.xml
+++ b/res/values-eu-rES/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefonoa"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefono-deien kudeaketa"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefonoa"</string>
<string name="unknown" msgid="6878797917991465859">"Ezezaguna"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Dei galdua"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Laneko dei bat galdu duzu"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Dei galduak"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> dei galdu"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Deitzaile honen dei galdua: <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Erantzun bizkorra"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mezua bidali da <xliff:g id="PHONE_NUMBER">%s</xliff:g> zenbakira."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Gailuaren jabeak larrialdi-deiak bakarrik egitea onartzen du"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Deiak egiteko kontuak"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Larrialdi-deiak bakarrik egin daitezke."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Aplikazioak irteerako deiak egin ahal izan ditzan, telefonoaren eginbidea erabiltzeko baimena behar du."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Deitzeko, idatzi balio duen zenbaki bat."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Une honetan ezin da deirik gehitu."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Desgaitu TTY modua bideo-deiak egiteko."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Erantzungailuaren zenbakia falta da"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Ez da erantzungailuaren zenbakirik gorde SIM txartelean."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Gehitu zenbakia"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Telefono-aplikazio lehenetsia aldatu nahi duzu?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> aplikazioaren ordez <xliff:g id="NEW_APP">%1$s</xliff:g> erabili nahi duzu telefono-aplikazio lehenetsi gisa?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> telefono-aplikazio lehenetsi gisa erabili nahi duzu?"</string>
</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 61d4ae1..c2f6ea9 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"تلفن"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"مدیریت تماس تلفنی"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"تلفن"</string>
<string name="unknown" msgid="6878797917991465859">"ناشناس"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"تماس بی پاسخ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"تماس کاری ازدسترفته"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"تماسهای بی پاسخ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> تماس بی پاسخ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"تماس بی پاسخ از <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"پاسخ سریع"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"پیام به <xliff:g id="PHONE_NUMBER">%s</xliff:g> ارسال شد."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"مالک دستگاه فقط تماسهای اضطراری را مجاز کرده است"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"حسابهای تماس"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"فقط تماسهای اضطراری مجاز است."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"این برنامه نمیتواند بدون اجازه تلفن، تماسهای خروجی برقرار کند."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"برای برقراری تماس، یک شماره معتبر وارد کنید."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"در این زمان نمیتوان تماسی اضافه کرد."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"لطفاً حالت TTY را برای برقراری تماسهای ویدیویی غیرفعال کنید."</string>
<string name="no_vm_number" msgid="4164780423805688336">"عدم وجود شماره پست صوتی"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"شماره پست صوتی در سیم کارت ذخیره نشده است."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"افزودن شماره"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"برنامه شمارهگیر پیشفرض تغییر کند؟"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"از <xliff:g id="NEW_APP">%1$s</xliff:g> به جای <xliff:g id="CURRENT_APP">%2$s</xliff:g> به عنوان برنامه شمارهگیر پیشفرض استفاده شود؟"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"از <xliff:g id="NEW_APP">%s</xliff:g> به عنوان برنامه شمارهگیر پیشفرض استفاده شود؟"</string>
</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 1ff2b0b..d318f6b 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -16,30 +16,36 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Puhelin"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Puhelujen hallinta"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Puhelin"</string>
<string name="unknown" msgid="6878797917991465859">"Tuntematon"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Vastaamatta jäänyt puhelu"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Vastaamaton työpuhelu"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Vastaamattomat puhelut"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> vastaamatonta puhelua"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Vastaamatta jäänyt puhelu numerosta <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Soita takaisin"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Soita"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Viesti"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Puhelu mykistetty."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Kaiutin käytössä."</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"En voi puhua nyt. Mikä hätänä?"</string>
- <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Soitan sinulle heti takaisin."</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"En voi vastata - mitä asiaa?"</string>
+ <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Soitan sinulle pian."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Soitan sinulle myöhemmin."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"En voi puhua nyt. Soita myöhemmin?"</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"En voi vastata. Soitatko myöhemmin?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Pikavastaukset"</string>
<string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Muokkaa pikavastausta"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Pikavastaukset"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Viesti lähetetty numeroon <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Laitteen omistaja on sallinut vain hätäpuhelut"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Puhelutilit"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Vain hätäpuhelut sallittu"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Tämä sovellus ei voi soittaa puheluita ilman Puhelin-lupaa."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Soita antamalla kelvollinen numero."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Puhelua ei voi lisätä juuri nyt."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Poista TTY-tila käytöstä, jos haluat soittaa videopuheluita."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Puhelinvastaajan numero puuttuu"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM-kortille ei ole tallennettu puhelinvastaajan numeroa."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Lisää numero"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Vaihdetaanko oletuspuhelusovellus?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Asetetaanko oletuspuhelusovellukseksi <xliff:g id="NEW_APP">%1$s</xliff:g> sovelluksen <xliff:g id="CURRENT_APP">%2$s</xliff:g> sijaan?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Asetetaanko <xliff:g id="NEW_APP">%s</xliff:g> oletuspuhelusovellukseksi?"</string>
</resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 1aa0bdb..d88a8ed 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Téléphone"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Gestion des appels téléphoniques"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Téléphone"</string>
<string name="unknown" msgid="6878797917991465859">"Inconnu"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Appel manqué"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Appel professionnel manqué"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Appels manqués"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> appels manqués"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Appel manqué de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Réponse rapide"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Message envoyé à <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Seuls les appels d\'urgence sont autorisés par le propriétaire de l\'appareil"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Comptes d\'appel"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Seuls les appels d\'urgence sont autorisés."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Cette application ne peut pas faire d\'appels sans l\'autorisation de l\'application Téléphone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Pour faire un appel, entrez un numéro valide."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Impossible d\'ajouter l\'appel pour le moment."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Veuillez désactiver le mode ATS pour faire un appel vidéo."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Numéro de messagerie vocale manquant"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Aucun numéro de messagerie vocale n\'est enregistré sur la carte SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Ajouter un numéro"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Changer l\'application de composition par défaut?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Utiliser <xliff:g id="NEW_APP">%1$s</xliff:g> au lieu de <xliff:g id="CURRENT_APP">%2$s</xliff:g> comme application de composition par défaut?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Utiliser <xliff:g id="NEW_APP">%s</xliff:g> comme application de composition par défaut?"</string>
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index f5f8afd..9962c6e 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Téléphone"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Gestion des appels téléphoniques"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Téléphone"</string>
<string name="unknown" msgid="6878797917991465859">"Inconnu"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Appel manqué"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Appel professionnel manqué"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Appels manqués"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> appels manqués"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Appel manqué de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -31,15 +33,19 @@
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Je t\'appellerai plus tard."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Peux pas parler. On se rappelle ?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Réponses rapides"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Modifier les réponses rapides"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Modifier réponses rapides"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Réponse rapide"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Message envoyé à <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Le propriétaire de l\'appareil n\'autorise que les appels d\'urgence."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Comptes téléphoniques"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Seuls les appels d\'urgence sont autorisés."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Cette application ne peut pas passer d\'appels sortants sans l\'autorisation de l\'application Téléphone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Pour émettre un appel, veuillez saisir un numéro valide."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Impossible d\'ajouter un appel pour le moment."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Veuillez désactiver le mode TTY pour passer des appels vidéo."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Numéro de messagerie vocale manquant"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Aucun numéro de messagerie vocale n\'est enregistré sur la carte SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Ajouter un numéro"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Modifier l\'application de clavier par défaut ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Utiliser <xliff:g id="NEW_APP">%1$s</xliff:g> et non plus <xliff:g id="CURRENT_APP">%2$s</xliff:g> comme application de clavier par défaut ?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Utiliser <xliff:g id="NEW_APP">%s</xliff:g> comme application de clavier par défaut ?"</string>
</resources>
diff --git a/res/values-gl-rES/strings.xml b/res/values-gl-rES/strings.xml
index 628caa9..ef36d3a 100644
--- a/res/values-gl-rES/strings.xml
+++ b/res/values-gl-rES/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Teléfono"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Xestión de chamadas do teléfono"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Teléfono"</string>
<string name="unknown" msgid="6878797917991465859">"Descoñecido"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Chamada perdida"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Chamada de traballo perdida"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Chamadas perdidas"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> chamadas perdidas"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Chamada perdida de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Resposta rápida"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mensaxe enviada ao <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"O propietario do dispositivo só permite as chamadas de emerxencia"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Contas de chamadas"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Só se permiten chamadas de emerxencia."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Esta aplicación non pode facer chamadas saíntes sen permiso do teléfono."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Para realizar unha chamada, introduce un número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Neste momento non se pode engadir a chamada."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Desactiva o modo TTY para realizar videochamadas."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Falta o número de correo de voz"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Non hai ningún número de correo de voz almacenado na tarxeta SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Engadir número"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Queres cambiar a aplicación predeterminada do marcador?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Queres usar <xliff:g id="NEW_APP">%1$s</xliff:g> en lugar de <xliff:g id="CURRENT_APP">%2$s</xliff:g> como a túa aplicación de marcador predeterminada?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Queres usar <xliff:g id="NEW_APP">%s</xliff:g> como a túa aplicación de marcador predeterminada?"</string>
</resources>
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml
index 8d0ad60..f5db5ce 100644
--- a/res/values-gu-rIN/strings.xml
+++ b/res/values-gu-rIN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ફોન"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ફોન કૉલ સંચાલન"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ફોન"</string>
<string name="unknown" msgid="6878797917991465859">"અજાણ્યું"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"છૂટેલો કૉલ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"ચૂકી ગયેલ કાર્ય કૉલ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"છૂટેલા કૉલ્સ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> છૂટેલા કૉલ્સ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> નો કૉલ ચૂકી ગયાં"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ઝડપી પ્રતિસાદ"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર સંદેશ મોકલ્યો."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ઉપકરણના માલિક દ્વારા ફક્ત કટોકટીના કૉલ્સને મંજૂરી અપાયેલ છે"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"કૉલિંગ એકાઉન્ટ્સ"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"ફક્ત કટોકટીના કૉલ્સને મંજૂરી છે."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ફોન પરવાનગી વિના આ એપ્લિકેશન આઉટગોઇંગ કૉલ્સ કરી શકતી નથી."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"કૉલ કરવા માટે, માન્ય નંબર દાખલ કરો."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"આ સમયે કૉલ ઉમેરી શકાતો નથી."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"કૃપા કરીને વિડિઓ કૉલ્સ કરવા માટે TTY મોડ અક્ષમ કરો."</string>
<string name="no_vm_number" msgid="4164780423805688336">"વૉઇસમેઇલ નંબર ખૂટે છે"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM કાર્ડ પર કોઈ વૉઇસમેઇલ નંબર સંગ્રહિત નથી."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"નંબર ઉમેરો"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ડિફોલ્ટ ડાયલર એપ્લિકેશન બદલીએ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"તમારી ડિફોલ્ટ ડાયલર એપ્લિકેશન તરીકે <xliff:g id="CURRENT_APP">%2$s</xliff:g> ને બદલે <xliff:g id="NEW_APP">%1$s</xliff:g> નો ઉપયોગ કરીએ?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"તમારી ડિફોલ્ટ ડાયલર એપ્લિકેશન તરીકે <xliff:g id="NEW_APP">%s</xliff:g> નો ઉપયોગ કરીએ?"</string>
</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 760fe63..463cdc4 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"फ़ोन"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"फ़ोन कॉल प्रबंधन"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"फ़ोन"</string>
<string name="unknown" msgid="6878797917991465859">"अज्ञात"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"छूटी कॉल"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"कार्यस्थल का छूटा हुआ कॉल"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"छूटी कॉल"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> छूटी कॉल"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> की कॉल छूटी"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"झटपट उत्तर"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> को संदेश भेजा गया."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"डिवाइस स्वामी द्वारा केवल आपातकालीन कॉल करने की अनुमति है"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"कॉलिंग खाते"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"केवल आपातकालीन कॉल की अनुमति है."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"यह ऐप्लिकेशन फ़ोन अनुमति के बिना आउटगोइंग कॉल नहीं कर सकता."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"कॉल करने के लिए, मान्य नंबर डालें."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"इस समय कॉल नहीं जोड़ा जा सकता."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"कृपया वीडियो कॉल करने के लिए TTY मोड अक्षम करें."</string>
<string name="no_vm_number" msgid="4164780423805688336">"गुम वॉयस मेल नंबर"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"सिम कार्ड पर कोई वॉयस मेल नंबर संग्रहीत नहीं है."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"नंबर जोड़ें"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"डिफ़ॉल्ट डायलर ऐप को बदलें?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"अपने डिफ़ॉल्ट डायलर ऐप के रूप में <xliff:g id="CURRENT_APP">%2$s</xliff:g> के बजाय <xliff:g id="NEW_APP">%1$s</xliff:g> का उपयोग करें?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"अपने डिफ़ॉल्ट डायलर ऐप के रूप में <xliff:g id="NEW_APP">%s</xliff:g> का उपयोग करें?"</string>
</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 5242229..5022e7e 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Upravljanje telefonskim pozivima"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Nepoznato"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Propušteni poziv"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Propušten poslovni poziv"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Propušteni pozivi"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"Broj propuštenih poziva: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Propušten poziv kontakta <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -29,17 +31,21 @@
<string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Sada ne mogu razgovarati. Što ima?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Nazvat ću vas odmah."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Zvat ću vas kasnije."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Sada ne mogu razgovarati. Nazovite me kasnije?"</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Sada ne mogu razgovarati. Nazovite me kasnije."</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Brzi odgovori"</string>
<string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Uređivanje brzih odgovora"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Brzi odgovor"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Poruka poslana na broj <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Vlasnik uređaja dopušta samo hitne pozive"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Računi za pozivanje"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Dopušteni su samo hitni pozivi."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ova aplikacija ne može uspostavljati odlazne pozive bez dopuštenja za telefon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Unesite važeći broj da biste uspostavili poziv."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Poziv trenutačno nije moguć."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Onemogućite TTY način da biste omogućili videopozive."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Nedostaje broj govorne pošte"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Na SIM kartici nije spremljen broj govorne pošte."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Dodaj broj"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Želite li promijeniti zadanu aplikaciju brojčanika?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Želite li upotrebljavati <xliff:g id="NEW_APP">%1$s</xliff:g> umjesto aplikacije <xliff:g id="CURRENT_APP">%2$s</xliff:g> kao zadanu aplikaciju brojčanika?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Želite li upotrebljavati <xliff:g id="NEW_APP">%s</xliff:g> kao zadanu aplikaciju brojčanika?"</string>
</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 69fb335..ab39a81 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Híváskezelés"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Ismeretlen"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Nem fogadott hívás"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Nem fogadott munkahelyi hívás"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Nem fogadott hívások"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> nem fogadott hívás"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Nem fogadott hívás: <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Gyors válasz"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Üzenet elküldve ide: <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Az eszköz tulajdonosa csak a segélyhívásokat engedélyezte"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Telefonos fiókok"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Csak vészhívás engedélyezett."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Az alkalmazásból nem lehet kimenő hívást kezdeményezni a Telefon (Phone) engedély nélkül."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Hívásindításhoz adjon meg egy érvényes számot."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Jelenleg nem lehet videohívást hozzáadni."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Videohívások indításához kapcsolja ki a TTY módot."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Hiányzik a hangposta száma"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Nincs hangpostaszám a SIM kártyán."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Szám hozzáadása"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Módosítja az alapértelmezett tárcsázó alkalmazást?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Legyen a(z) <xliff:g id="NEW_APP">%1$s</xliff:g> az alapértelmezett tárcsázó alkalmazás a(z) <xliff:g id="CURRENT_APP">%2$s</xliff:g> helyett?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Legyen a(z) <xliff:g id="NEW_APP">%s</xliff:g> az alapértelmezett tárcsázó alkalmazás?"</string>
</resources>
diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml
index c38c6fc..56773ea 100644
--- a/res/values-hy-rAM/strings.xml
+++ b/res/values-hy-rAM/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Հեռախոս"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Հեռախոսային զանգերի կառավարում"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Հեռախոս"</string>
<string name="unknown" msgid="6878797917991465859">"Անհայտ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Բաց թողնված զանգ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Բաց թողնված աշխատանքային զանգ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Բաց թողնված զանգեր"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> բաց թողնված զանգ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Բաց թողնված զանգ <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>-ից"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Արագ պատասխան"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Հաղորդագրությունն ուղարկվել է <xliff:g id="PHONE_NUMBER">%s</xliff:g>-ին:"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Սարքի սեփականատերը թույլատրում է միայն արտակարգ իրավիճակի զանգերը"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Հաշիվներ զանգերի համար"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Միայն արտակարգ իրավիճակների զանգերն են թույլատրվում:"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Առանց Հեռախոսի թույլտվության այս ծրագիրը չի կարող ելքային զանգեր կատարել:"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Զանգ կատարելու համար մուտքագրեք ճիշտ համար:"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Հնարավոր չէ ևս մեկ զանգ ավելացնել այս պահին:"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Տեսազանգեր կատարելու համար անջատեք TTY ռեժիմը:"</string>
<string name="no_vm_number" msgid="4164780423805688336">"Բացակայում է ձայնային փոստի համարը"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM քարտում ձայնային փոստի ոչ մի համար գրանցված չէ:"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Ավելացնել համար"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Փոխե՞լ կանխադրված Համարհավաքի հավելվածը:"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Օգտագործե՞լ <xliff:g id="NEW_APP">%1$s</xliff:g> հավելվածը <xliff:g id="CURRENT_APP">%2$s</xliff:g>-ի փոխարեն որպես համարհավաքի կանխադրված հավելված:"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Դարձնե՞լ <xliff:g id="NEW_APP">%s</xliff:g> հավելվածը համարհավաքի կանխադրված հավելված:"</string>
</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 7ad8e00..d8f823f 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -16,13 +16,15 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telepon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Pengelolaan Panggilan Telepon"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telepon"</string>
<string name="unknown" msgid="6878797917991465859">"Tidak diketahui"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Panggilan tak terjawab"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Panggilan tak terjawab di telepon kerja"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Panggilan tak terjawab"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> panggilan tak terjawab"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Panggilan tak terjawab dari <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Hubungi kembali"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Telepon"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Pesan"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Panggilan disenyapkan."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Pengeras suara ponsel diaktifkan."</string>
@@ -30,16 +32,20 @@
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Saya segera telepon balik."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Nanti saya telepon balik."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Tak bisa bicara skrg. Tlp lg nanti?"</string>
- <string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Tanggapan cepat"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Edit tanggapan cepat"</string>
+ <string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Respons cepat"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Edit respons cepat"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
- <string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Tanggapan cepat"</string>
+ <string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Respons cepat"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Pesan dikirim ke <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Hanya panggilan darurat yang diizinkan oleh pemilik perangkat"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Akun pemanggil"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Hanya panggilan darurat yang diizinkan."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Aplikasi ini tidak dapat melakukan panggilan keluar tanpa izin Telepon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Untuk melakukan panggilan telepon, masukkan nomor yang valid."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Panggilan tidak dapat ditambahkan untuk saat ini."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Nonaktifkan Mode TTY untuk melakukan panggilan video."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Nomor kotak pesan hilang"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Tidak ada nomor kotak pesan tersimpan pada kartu SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Tambahkan nomor"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Ubah aplikasi Pemanggil default?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Gunakan <xliff:g id="NEW_APP">%1$s</xliff:g>, bukan <xliff:g id="CURRENT_APP">%2$s</xliff:g> sebagai aplikasi pemanggil default?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Gunakan <xliff:g id="NEW_APP">%s</xliff:g> sebagai aplikasi pemanggil default?"</string>
</resources>
diff --git a/res/values-is-rIS/strings.xml b/res/values-is-rIS/strings.xml
index 13827b9..56610c5 100644
--- a/res/values-is-rIS/strings.xml
+++ b/res/values-is-rIS/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Sími"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Símtalastjórnun"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Sími"</string>
<string name="unknown" msgid="6878797917991465859">"Óþekkt"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Ósvarað símtal"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Ósvarað vinnusímtal"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Ósvöruð símtöl"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ósvöruð símtöl"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Ósvarað símtal frá <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Snarsvar"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Skilaboð send til <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Eigandi tækisins leyfir aðeins neyðarsímtöl"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Símtalareikningar"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Aðeins neyðarsímöl eru leyfð."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Þetta forrit getur ekki hringt án heimildar í símanum."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Sláðu inn gilt númer til að hringja símtal."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Ekki er hægt að bæta símtali við sem stendur."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Slökktu á fjarritastillingu til að hringja myndsímtöl."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Talhólfsnúmer vantar"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Ekkert talhólfsnúmer er vistað á SIM-kortinu."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Bæta númeri við"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Viltu skipta um sjálfgefið hringiforrit?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Nota <xliff:g id="NEW_APP">%1$s</xliff:g> í stað <xliff:g id="CURRENT_APP">%2$s</xliff:g> sem sjálfgefið hringiforrit?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Nota <xliff:g id="NEW_APP">%s</xliff:g> sem sjálfgefið hringiforrit?"</string>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 0505e37..47c5a1b 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefono"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Phone Call Management"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefono"</string>
<string name="unknown" msgid="6878797917991465859">"Sconosciuto"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Chiamata senza risposta"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Chiamata di lavoro persa"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Chiamate senza risposta"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> chiamate senza risposta"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Chiamata senza risposta da <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Risposta rapida"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Messaggio inviato a <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Il proprietario del dispositivo consente soltanto chiamate di emergenza"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Account di chiamata"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Sono consentite soltanto le chiamate di emergenza."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Non è possibile effettuare chiamate tramite questa applicazione senza l\'autorizzazione sul telefono."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Per effettuare una chiamata, inserisci un numero valido."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Al momento non è possibile aggiungere la chiamata."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Per poter fare videochiamate devi disattivare la modalità TTY."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Numero segreteria mancante"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Nessun numero di segreteria presente nella SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Aggiungi numero"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Modificare l\'app tastiera predefinita?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Utilizzare <xliff:g id="NEW_APP">%1$s</xliff:g> invece di <xliff:g id="CURRENT_APP">%2$s</xliff:g> come app tastiera predefinita?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Utilizzare <xliff:g id="NEW_APP">%s</xliff:g> come app tastiera predefinita?"</string>
</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index d8bdb1e..af490ca 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"טלפון"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ניהול שיחות טלפון"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"טלפון"</string>
<string name="unknown" msgid="6878797917991465859">"לא ידוע"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"שיחה שלא נענתה"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"שיחה עסקית שלא נענתה"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"שיחות שלא נענו"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> שיחות שלא נענו"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"שיחה שלא נענתה מאת <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"תגובה מהירה"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"הודעה נשלחה אל <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"בעלי המכשיר מתיר לבצע שיחות חירום בלבד"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"חשבונות לביצוע שיחות"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"ניתן לבצע רק שיחות חירום."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"לא ניתן לבצע שיחות יוצאות באמצעות האפליקציה הזו ללא ההרשאה \'טלפון\'."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"כדי להתקשר, הזן מספר טלפון חוקי."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"לא ניתן כעת להוסיף את השיחה."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"השבת את מצב TTY כדי לבצע שיחות וידאו."</string>
<string name="no_vm_number" msgid="4164780423805688336">"חסר מספר של דואר קולי"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"בכרטיס ה-SIM לא מאוחסן מספר של דואר קולי."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"הוסף מספר"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"האם לשנות את אפליקציית החייגן שבברירת מחדל?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"האם להשתמש ב-<xliff:g id="NEW_APP">%1$s</xliff:g> במקום ב-<xliff:g id="CURRENT_APP">%2$s</xliff:g> כאפליקציית החייגן שבברירת מחדל?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"האם להשתמש ב-<xliff:g id="NEW_APP">%s</xliff:g> כאפליקציית החייגן שבברירת מחדל?"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 551f281..07e9c9f 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"電話"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"通話管理"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"電話"</string>
<string name="unknown" msgid="6878797917991465859">"通知不可能"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"不在着信"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"仕事の通話の不在着信"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"不在着信"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"不在着信<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>件"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>さんからの不在着信"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"クイック返信"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g>にメッセージを送信しました。"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"端末の所有者に許可されているのは緊急通報のみです。"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"通話アカウント"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"許可されているのは緊急通報のみです。"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"このアプリは、電話権限がないため発信できません。"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"発信するには、有効な番号を入力してください。"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"通話は現在追加できません。"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"ビデオハングアウトを行うにはTTYモードを無効にしてください。"</string>
<string name="no_vm_number" msgid="4164780423805688336">"ボイスメール番号がありません"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIMカードにボイスメールの番号がありません。"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"番号を追加"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"既定の電話アプリを変更しますか?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g>を<xliff:g id="CURRENT_APP">%2$s</xliff:g>の代わりに既定の電話アプリとして使用しますか?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g>を既定の電話アプリとして使用しますか?"</string>
</resources>
diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml
index ac1c20b..b8986a2 100644
--- a/res/values-ka-rGE/strings.xml
+++ b/res/values-ka-rGE/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ტელეფონი"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ტელეფონის ზარების მართვა"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ტელეფონი"</string>
<string name="unknown" msgid="6878797917991465859">"უცნობი"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"გამოტოვებული ზარი"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"გამოტოვებული ზარი (სამსახური)"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"გამოტოვებული ზარები"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> გამოტოვებული ზარები"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"გამოტოვებული ზარი <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>-ისგან"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"სწრაფი პასუხი"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"შეტყობინება გაიგზავნა <xliff:g id="PHONE_NUMBER">%s</xliff:g>-თან."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"მოწყობილობის მფლობელის მიერ ნებადართულია მხოლოდ საგანგებო ზარების განხორციელება"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"დარეკვის ანგარიშები"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"შესაძლებელია მხოლოდ გადაუდებელი ზარების განხორციელება."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ეს აპლიკაცია ტელეფონის ნებართვის გარეშე გამავალ ზარებს ვერ განახორციელებს."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ზარის განხორციელებისათვის, შეიყვანეთ მოქმედი ნომერი."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ამ ეტაპზე ზარის დამატება ვერ ხერხდება."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"ვიდეოზარების განხორციელებისათვის, გთხოვთ, გამორთოთ TTY რეჟიმი"</string>
<string name="no_vm_number" msgid="4164780423805688336">"ხმოვანი ფოსტის ნომერი არ არის"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM ბარათზე ხმოვანი ფოსტის ნომერი შენახული არ არის."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"ნომრის დამატება"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"გსურთ ამკრეფის ნაგულისხმევი აპის შეცვლა?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"გსურთ ამკრეფის ნაგულისხმევ აპად <xliff:g id="CURRENT_APP">%2$s</xliff:g>-ის ნაცვლად <xliff:g id="NEW_APP">%1$s</xliff:g>-ის გამოყენება?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"გსურთ, გამოიყენოთ <xliff:g id="NEW_APP">%s</xliff:g>, როგორც ამკრეფის ნაგულისხმევი აპი?"</string>
</resources>
diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk-rKZ/strings.xml
index 89be55c..baf6ef7 100644
--- a/res/values-kk-rKZ/strings.xml
+++ b/res/values-kk-rKZ/strings.xml
@@ -16,13 +16,15 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Телефон"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Телефон қоңырауларын басқару"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
<string name="unknown" msgid="6878797917991465859">"Белгісіз"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Қабылданбаған қоңырау"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Өткізіп алынған жұмыс қоңырауы"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Қабылданбаған қоңыраулар"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> қабылданбаған қоңыраулар"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> қоңырауы қабылданбаған"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Кері қоңырау шалу"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Қоңырау шалу"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Хабар"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Қоңырау үнсіздендірілген."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Үндеткішті телефон қосылды."</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Жылдам жауап"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Хабар <xliff:g id="PHONE_NUMBER">%s</xliff:g> нөміріне жіберілді."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Құрылғы иесі тек жедел қоңырауларға рұқсат еткен"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Қоңырау шалу есептік жазбалары"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Тек төтенше қоңырауларға рұқсат етілген."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"\"Телефон\" рұқсатынсыз бұл қолданба шығыс қоңырауларды соға алмайды."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Қоңырау шалу үшін жарамды нөмірді енгізіңіз."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Қоңырауды қазіргі уақытта қосу мүмкін емес."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Бейне қоңырау шалу үшін Телетайп режимін өшіріңіз."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Дауыс хабарының нөмірі жоқ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM картасында ешқандай дауыс хабарының нөмірі сақталмаған."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Нөмір қосу"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Әдепкі нөмір тергіш қолданбаны өзгерткіңіз келе ме?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> орнына <xliff:g id="NEW_APP">%1$s</xliff:g> қолданбасын әдепкі тергіш қолданба ретінде пайдалану қажет пе?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> қолданбасын әдепкі нөмір тергіш қолданба ретінде пайдалану қажет пе?"</string>
</resources>
diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml
index 1a019e8..39cbf3d 100644
--- a/res/values-km-rKH/strings.xml
+++ b/res/values-km-rKH/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ទូរស័ព្ទ"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ការគ្រប់គ្រងការហៅទូរស័ព្ទ"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ទូរស័ព្ទ"</string>
<string name="unknown" msgid="6878797917991465859">"មិនស្គាល់"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"ខកខានទទួល"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"បានខកខានការហៅចូលពីកន្លែងការងារ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"ខកខានទទួល"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ខកខានការទទួល"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"ខកខានទទួលពី <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ឆ្លើយតបរហ័ស"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"បានផ្ញើសារទៅ <xliff:g id="PHONE_NUMBER">%s</xliff:g> ។"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ការហៅពេលមានអាសន្នដែលត្រូវបានអនុញ្ញាតដោយម្ចាស់ឧបករណ៍តែប៉ុណ្ណោះ។"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"គណនីហៅទូរស័ព្ទ"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"អនុញ្ញាតតែការហៅពេលមានអាសន្នប៉ុណ្ណោះ"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"កម្មវិធីនេះមិនអាចធ្វើការហៅចេញដោយគ្មានការអនុញ្ញាត ទូរស័ព្ទ បានទេ។"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ដើម្បីធ្វើការហៅ បញ្ចូលលេខដែលមានសុពលភាព។"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"មិនអាចបន្ថែមនៅពេលនេះបានទេ។"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"សូមបិទដំណើរការរបៀប TTY ដើម្បីធ្វើការហៅជាវីដេអូ។"</string>
<string name="no_vm_number" msgid="4164780423805688336">"បាត់ចំនួនសារជាសំឡេង"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"គ្មានចំនួនសារជាសំឡេងត្រូវបានរក្សាទុកនៅលើស៊ីមកាតទេ។"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"បន្ថែមលេខ"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ប្តូរកម្មវិធីហៅទូរស័ព្ទលំនាំដើម?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"ប្រើ <xliff:g id="NEW_APP">%1$s</xliff:g> ជំនួសឲ្យ <xliff:g id="CURRENT_APP">%2$s</xliff:g> ជាកម្មវិធីហៅទូរស័ព្ទលំនាំដើមរបស់អ្នក?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"ប្រើ <xliff:g id="NEW_APP">%s</xliff:g> ជាកម្មវិធីហៅទូរស័ព្ទលំនាំដើមរបស់អ្នក?"</string>
</resources>
diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn-rIN/strings.xml
index e84afc9..60f8caa 100644
--- a/res/values-kn-rIN/strings.xml
+++ b/res/values-kn-rIN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ಫೋನ್"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ಫೋನ್ ಕರೆ ನಿರ್ವಹಣೆ"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ಫೋನ್"</string>
<string name="unknown" msgid="6878797917991465859">"ಅಜ್ಞಾತ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"ಮಿಸ್ಡ್ ಕಾಲ್"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"ಮಿಸ್ಡ್ ಕೆಲಸದ ಕರೆ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"ತಪ್ಪಿದ ಕರೆಗಳು"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ತಪ್ಪಿದ ಕರೆಗಳು"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> ಅವರಿಂದ ಮಿಸ್ಡ್ ಕಾಲ್"</string>
@@ -26,20 +28,24 @@
<string name="notification_missedCall_message" msgid="3049928912736917988">"ಸಂದೇಶ"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"ಕರೆಯನ್ನು ಮ್ಯೂಟ್ ಮಾಡಲಾಗಿದೆ."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"ಸ್ಪೀಕರ್ಫೋನ್ ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ."</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"ಇದೀಗ ಮಾತನಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಏನು ಸಮಾಚಾರ?"</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"ಈಗ ಮಾತನಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಏನು ವಿಷಯ?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"ನಾನು ಮರಳಿ ನಿಮಗೆ ಕರೆ ಮಾಡುತ್ತೇನೆ."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"ನಾನು ನಂತರ ನಿಮಗೆ ಕರೆ ಮಾಡುತ್ತೇನೆ."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"ಇದೀಗ ಮಾತನಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ನನಗೆ ನಂತರ ಕರೆ ಮಾಡುವಿರಾ?"</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"ಈಗ ಮಾತನಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ನಂತರ ಮಾಡುವಿರಾ?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"ತ್ವರಿತ ಪ್ರತಿಕ್ರಿಯೆಗಳು"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"ತ್ವರಿತ ಪ್ರತಿಕ್ರಿಯೆಗಳನ್ನು ಸಂಪಾದಿಸಿ"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"ತ್ವರಿತ ಪ್ರತಿಕ್ರಿಯೆ ಸಂಪಾದಿಸಿ"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ತ್ವರಿತ ಪ್ರತಿಕ್ರಿಯೆ"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> ಗೆ ಸಂದೇಶ ಕಳುಹಿಸಲಾಗಿದೆ."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ಸಾಧನದ ಮಾಲೀಕರಿಂದ ತುರ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಮಾತ್ರ ಅವಕಾಶವಿದೆ"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"ಕರೆ ಮಾಡುವ ಖಾತೆಗಳು"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"ತುರ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಮಾತ್ರ ಅವಕಾಶವಿದೆ."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ಈ ಅಪ್ಲಿಕೇಶನ್ ಫೋನ್ ಅನುಮತಿಯಿಲ್ಲದೆ ಹೊರಹೋಗುವ ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ಕರೆಯನ್ನು ಮಾಡಲು, ಮಾನ್ಯವಾದ ಸಂಖ್ಯೆಯನ್ನು ನಮೂದಿಸಿ."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ಕರೆಯನ್ನು ಈ ಸಮಯದಲ್ಲಿ ಸೇರಿಸಲಾಗುವುದಿಲ್ಲ."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"ವೀಡಿಯೊ ಕರೆಗಳನ್ನು ಮಾಡಲು TTY ಮೋಡ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ."</string>
<string name="no_vm_number" msgid="4164780423805688336">"ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯು ಕಾಣೆಯಾಗಿದೆ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"ಸಿಮ್ ಕಾರ್ಡ್ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ಡೀಫಾಲ್ಟ್ ಡಯಲರ್ ಅಪ್ಲಿಕೇಶನ್ ಬದಲಾಯಿಸುವುದೇ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> ಬದಲಿಗೆ <xliff:g id="NEW_APP">%1$s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಡಯಲರ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಬಳಸುವುದೇ?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಡಯಲರ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಬಳಸುವುದೇ?"</string>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index efb59cd..4c51e98 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"전화"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"통화 관리"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"전화"</string>
<string name="unknown" msgid="6878797917991465859">"알 수 없음"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"부재중 전화"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"부재중 업무 통화"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"부재중 통화"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"부재중 통화 <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>통"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>의 부재중 전화"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"빠른 응답"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g>(으)로 메시지를 보냈습니다."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"기기 소유자만 긴급 전화를 사용할 수 있습니다."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"통화 계정"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"긴급 전화만 허용됩니다."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"전화 권한이 없으므로 애플리케이션에서 발신 전화를 걸 수 없습니다."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"전화를 걸려면 올바른 번호를 입력하세요."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"현재 통화를 추가할 수 없습니다."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"화상 통화를 하려면 TTY 모드를 중지하세요."</string>
<string name="no_vm_number" msgid="4164780423805688336">"음성사서함 번호 없음"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM 카드에 저장된 음성사서함 번호가 없습니다."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"번호 추가"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"기본 다이얼러 앱을 변경하시겠습니까?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> 대신 <xliff:g id="NEW_APP">%1$s</xliff:g>을(를) 기본 다이얼러 앱으로 사용하시겠습니까?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g>을(를) 기본 다이얼러 앱으로 사용하시겠습니까?"</string>
</resources>
diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml
index 6b5e9c3..c805165 100644
--- a/res/values-ky-rKG/strings.xml
+++ b/res/values-ky-rKG/strings.xml
@@ -16,42 +16,36 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Телефон"</string>
- <!-- no translation found for unknown (6878797917991465859) -->
- <skip />
- <!-- no translation found for notification_missedCallTitle (7554385905572364535) -->
- <skip />
- <!-- no translation found for notification_missedCallsTitle (1361677948941502522) -->
- <skip />
- <!-- no translation found for notification_missedCallsMsg (4575787816055205600) -->
- <skip />
- <!-- no translation found for notification_missedCallTicker (504686252427747209) -->
- <skip />
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Телефон чалууларын башкаруу"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
+ <string name="unknown" msgid="6878797917991465859">"Белгисиз"</string>
+ <string name="notification_missedCallTitle" msgid="7554385905572364535">"Кабыл алынбаган чалуу"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Кабыл алынбай калган чалуу (жумуш)"</string>
+ <string name="notification_missedCallsTitle" msgid="1361677948941502522">"Кабыл алынбаган чалуулар"</string>
+ <string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> кабыл алынбаган чалуу"</string>
+ <string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> дегенден кабыл алынбаган чалуу"</string>
<string name="notification_missedCall_call_back" msgid="2684890353590890187">"Кайра чалуу"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Билдирүү"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Чалуу үнсүз тартипте."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Динамик иштеп жатат."</string>
- <!-- no translation found for respond_via_sms_canned_response_1 (2461606462788380215) -->
- <skip />
- <!-- no translation found for respond_via_sms_canned_response_2 (4074450431532859214) -->
- <skip />
- <!-- no translation found for respond_via_sms_canned_response_3 (3496079065723960450) -->
- <skip />
- <!-- no translation found for respond_via_sms_canned_response_4 (1698989243040062190) -->
- <skip />
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Азыр сүйлөшө албайм. Эмне болду?"</string>
+ <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Кайра чалам."</string>
+ <string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Кийинчерээк чалып коём."</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Сүйлөшө албайм. Кийин чаласызбы?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Тез жооптор"</string>
<string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Тез жоопторду өзгөртүү"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Тез жооп"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> номуруна билдирүү жөнөтүлдү."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Түзмөк ээси шашылыш чалууларга гана уруксат берген"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Чалуу каттоо эсептери"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Шашылыш чалууларга гана уруксат берилген."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Бул колдонмо тийиштүү уруксатсыз чалууларды жасай албайт."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Чалуу үчүн, жарактуу номер киргизиңиз."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Бул жолу чалууну кошуу мүмкүн эмес."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Видео чалууларды аткаруу үчүн Телетайп түзмөк режимин өчүрүңүз."</string>
- <!-- no translation found for no_vm_number (4164780423805688336) -->
- <skip />
- <!-- no translation found for no_vm_number_msg (1300729501030053828) -->
- <skip />
- <!-- no translation found for add_vm_number_str (4676479471644687453) -->
- <skip />
+ <string name="no_vm_number" msgid="4164780423805688336">"Үн почтасынын номери жок болуп жатат"</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"SIM-картада сакталган үн почтасынын номери жок."</string>
+ <string name="add_vm_number_str" msgid="4676479471644687453">"Номер кошуу"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Демейки номер тергич колдонмо өзгөрүлсүнбү?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Демейки номер тергич колдонмо катары мурунку <xliff:g id="CURRENT_APP">%2$s</xliff:g> колдонмонун ордуна <xliff:g id="NEW_APP">%1$s</xliff:g> бул колдонмо колдонулсунбу?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Демейки номер тергич колдонмо катары <xliff:g id="NEW_APP">%s</xliff:g> колдонулсунбу?"</string>
</resources>
diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml
index 52479c3..02eda11 100644
--- a/res/values-lo-rLA/strings.xml
+++ b/res/values-lo-rLA/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ໂທລະສັບ"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ການຈັດການການໂທລະສັບ"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ໂທລະສັບ"</string>
<string name="unknown" msgid="6878797917991465859">"ບໍ່ຮູ້ຈັກ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"ສາຍທີ່ບໍ່ໄດ້ຮັບ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"ສາຍບໍ່ໄດ້ຮັບຈາກບ່ອນເຮັດວຽກ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"ສາຍທີ່ບໍ່ໄດ້ຮັບ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ສາຍບໍ່ໄດ້ຮັບ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"ສາຍທີ່ບໍ່ໄດ້ຮັບຈາກ <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ຕອບກັບດ່ວນ"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"ສົ່ງຂໍ້ຄວາມຫາ <xliff:g id="PHONE_NUMBER">%s</xliff:g> ແລ້ວ."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ພຽງແຕ່ການໂທສຸກເສີນທີ່ໄດ້ຮັບອະນຸຍາດຈາກເຈົ້າຂອງອຸປະກອນເທົ່ານັ້ນ"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"ບັນຊີໂທ"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"ອະນຸຍາດໃຫ້ໂທສຸກເສີນເທົ່ານັ້ນ."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ແອັບພລິເຄຊັນນີ້ບໍ່ສາມາດໂທອອກໄດ້ ໂດຍບໍ່ມີການອະນຸຍາດຂອງໂທລະສັບ."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ເພື່ອທີ່ຈະໂທ, ປ້ອນເບີໂທທີ່ໃຊ້ໄດ້ເຂົ້າໄປ."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ບໍ່ສາມາດເພີ່ມການໂທໄດ້ໃນເວລານີ້."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"ກະລຸນາປິດໃຊ້ງານໂໝດ TTY ເພື່ອໂທວິດີໂອ."</string>
<string name="no_vm_number" msgid="4164780423805688336">"ບໍ່ມີເບີຂໍ້ຄວາມສຽງ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"ບໍ່ມີເບີຂໍ້ຄວາມສຽງຖືກບັນທຶກໃນ SIM card."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"ເພີ່ມໝາຍເລກ"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ປ່ຽນແປງແອັບແຜ່ນກົດມາດຕະຖານ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"ໃຊ້ <xliff:g id="NEW_APP">%1$s</xliff:g> ແທນ <xliff:g id="CURRENT_APP">%2$s</xliff:g> ເປັນແອັບແຜ່ນກົດມາດຕະຖານຂອງທ່ານ?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"ໃຊ້ <xliff:g id="NEW_APP">%s</xliff:g> ເປັນແອັບແຜ່ນກົດມາດຕະຖານຂອງທ່ານ?"</string>
</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 8b0f066..c801acd 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefonas"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefono skambučių tvarkymas"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefonas"</string>
<string name="unknown" msgid="6878797917991465859">"Nežinomas"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Praleistas skambutis"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Praleistas darbo skambutis"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Praleisti skambučiai"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> praleisti (-ų) skambučiai (-ų)"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"praleistas skambutis nuo <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Greitas atsakas"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Pranešimas išsiųstas numeriu <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Įrenginio savininkas leidžia skambinti tik pagalbos numeriais"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Skambinimo paskyros"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Leidžiami tik skambučiai pagalbos numeriu."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Naudojant šią programą negalima skambinti be telefono leidimo."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Kad galėtumėte paskambinti, įveskite tinkamą numerį."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Šiuo metu dar vieno skambučio atlikti negalima."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Išjunkite TTY režimą, kad galėtumėte atlikti vaizdo skambučius."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Trūksta balso pašto numerio"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM kortelėje nėra išsaugoto balso pašto numerio."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Pridėti numerį"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Pakeisti numatytąją numerio rinkiklio programą?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Naudoti „<xliff:g id="NEW_APP">%1$s</xliff:g>“ vietoje „<xliff:g id="CURRENT_APP">%2$s</xliff:g>“ kaip numatytąją numerio rinkiklio programą?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Naudoti „<xliff:g id="NEW_APP">%s</xliff:g>“ kaip numatytąją numerio rinkiklio programą?"</string>
</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 43f0027..8f86db9 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Tālrunis"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Tālruņa zvanu pārvaldība"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Tālrunis"</string>
<string name="unknown" msgid="6878797917991465859">"Nezināms"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Neatbildēts zvans"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Neatbildēts darba zvans"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Neatbildētie zvani"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> neatbildēts(-i) zvans(-i)"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Neatbildēts zvans no: <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Ātrā atbilde"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Ziņojums nosūt. uz šādu tālr. nr.: <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Ierīces īpašnieks ļauj veikt tikai ārkārtas zvanus."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Zvanu konti"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Ir atļauti tikai ārkārtas zvani."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Šajā lietojumprogrammā nevar veikt izejošos zvanus bez tālruņa atļaujas."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Lai veiktu zvanu, ievadiet derīgu numuru."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Šobrīd nevar pievienot zvanu."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Lai veiktu videozvanus, lūdzu, atspējojiet teksta tālruņa režīmu."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Trūkst balss pasta numura"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM kartē neviens balss pasta numurs nav saglabāts."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Pievienot numuru"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Vai mainīt numura sastādītāja noklusējuma lietotni?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Vai lietotnes <xliff:g id="CURRENT_APP">%2$s</xliff:g> vietā izmantot <xliff:g id="NEW_APP">%1$s</xliff:g> kā numura sastādītāja noklusējuma lietotni?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Vai izmantot <xliff:g id="NEW_APP">%s</xliff:g> kā numura sastādītāja noklusējuma lietotni?"</string>
</resources>
diff --git a/res/values-mk-rMK/strings.xml b/res/values-mk-rMK/strings.xml
index 9654e0b..6ce21c0 100644
--- a/res/values-mk-rMK/strings.xml
+++ b/res/values-mk-rMK/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Телефон"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Управување со телефонски повици"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
<string name="unknown" msgid="6878797917991465859">"Непознато"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Пропуштен повик"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Пропуштен работен повик"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Пропуштени повици"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> пропуштени повици"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Пропуштен повик од <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Брз одговор"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Порака е испратена на <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Дозволени се само итни повици од страна на сопственикот на уредот"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Сметки за повици"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Дозволени се само итни повици."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Оваа апликација не може да прави појдовни повици без дозволата Телефон."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"За да повикате, внесете важечки број."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Повикот не може да се додаде во моментов."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Оневозможете го режимот TTY за да остварите видеоповици."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Недостасува број на говорна пошта"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Нема мемориран број на говорна пошта на СИМ картичката."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Додај број"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Смени ја стандардната апликација Бирач?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Користи <xliff:g id="NEW_APP">%1$s</xliff:g> наместо <xliff:g id="CURRENT_APP">%2$s</xliff:g> како стандардна апликација за бирање?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Користи <xliff:g id="NEW_APP">%s</xliff:g> како стандардна апликација за бирање?"</string>
</resources>
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml
index 38fc0e6..e51f662 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml-rIN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ഫോണ്"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ഫോൺ കോൾ മാനേജ്മെന്റ്"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ഫോണ്"</string>
<string name="unknown" msgid="6878797917991465859">"അജ്ഞാതം"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"മിസ്ഡ് കോൾ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"മിസ്ഡ് ഔദ്യോഗിക കോൾ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"മിസ്ഡ് കോളുകൾ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> മിസ്ഡ് കോളുകൾ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> എന്നതിൽ നിന്നുള്ള മിസ്ഡ് കോൾ"</string>
@@ -31,15 +33,19 @@
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"ഞാൻ നിങ്ങളെ പിന്നീട് വിളിക്കാം."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"ഇപ്പോൾ സംസാരിക്കാനാകില്ല. എന്നെ പിന്നീട് വിളിക്കാമോ?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"ദ്രുത പ്രതികരണങ്ങൾ"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"ദ്രുത പ്രതികരണങ്ങൾ എഡിറ്റുചെയ്യുക"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"പ്രതികരണം എഡിറ്റുചെയ്യൂ"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ദ്രുത പ്രതികരണം"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> എന്നതിലേക്ക് സന്ദേശമയച്ചു."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ഉപകരണ ഉടമ അടിയന്തിര കോളുകൾ മാത്രമേ അനുവദിച്ചിട്ടുള്ളൂ"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"കോളിംഗ് അക്കൗണ്ട്"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"അടിയന്തിര കോളുകൾ മാത്രമേ അനുവദിച്ചിട്ടുള്ളൂ."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ഫോൺ അനുമതിയില്ലാതെ ഈ അപ്ലിക്കേഷന് ഔട്ട്ഗോയിംഗ് കോളുകൾ വിളിക്കാൻ കഴിയില്ല."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ഒരു കോൾ ചെയ്യുന്നതിന്, സാധുതയുള്ള നമ്പർ നൽകുക."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"കോൾ ഇപ്പോൾ ചേർക്കാനാകില്ല."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"വീഡിയോ കോളുകൾ നടത്താൻ TTY മോഡ് പ്രവർത്തനരഹിതമാക്കുക."</string>
<string name="no_vm_number" msgid="4164780423805688336">"വോയ്സ്മെയിൽ നമ്പർ കാണുന്നില്ല"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"സിം കാർഡിൽ വോയ്സ്മെയിൽ നമ്പറൊന്നും സംഭരിച്ചിട്ടില്ല."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"നമ്പർ ചേർക്കുക"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"സ്ഥിര ഡയലർ ആപ്പ് മാറ്റണോ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> എന്നതിനുപകരം <xliff:g id="NEW_APP">%1$s</xliff:g> എന്നതിനെ നിങ്ങളുടെ സ്ഥിര ഡയലർ ആപ്പ് ആയി ഉപയോഗിക്കണോ?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> എന്നതിനെ നിങ്ങളുടെ സ്ഥിര ഡയലർ ആപ്പ് ആയി ഉപയോഗിക്കണോ?"</string>
</resources>
diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml
index 828f347..12a17aa 100644
--- a/res/values-mn-rMN/strings.xml
+++ b/res/values-mn-rMN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Утас"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Гар утасны Дуудлагын Удирдлага"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Утас"</string>
<string name="unknown" msgid="6878797917991465859">"Тодорхойгүй"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Аваагүй дуудлага"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Аваагүй албаны дуудлага"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Аваагүй дуудлагууд"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> аваагүй дуудлага"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>-н аваагүй дуудлага"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Шуурхай хариу"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Зурвасыг <xliff:g id="PHONE_NUMBER">%s</xliff:g> руу илгээв."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Энэхүү төхөөрөмжийн эзэмшигч нь зөвхөн түргэн тусламжийн дуудлага хийх эрхтэй байна."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Дуудлагын эрхтэй бүртгэлүүд"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Зөвхөн яаралтай тусламжийн дуудлага хийх боломжтой."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Энэ апп нь утасны зөвшөөрөлгүйгээр дуудлага хийх боломжгүй."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Дуудлага хийхийн тулд хүчин төгөлдөр дугаар оруулна уу."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Одоо дуудлага нэмэх боломжгүй."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Видео дуудлага хийхийн тулд TTY горимыг идэвхгүй болгоно уу."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Дуут шуудангийн дугаар байхгүй"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM карт дээр дуут шуудангийн дугаар хадгалагдаагүй байна."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Дугаар нэмэх"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Үндсэн залгагч апп-ыг өөрчлөх үү?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g>-ыг <xliff:g id="CURRENT_APP">%2$s</xliff:g>-ын оронд таны үндсэн залгагч апп болгон тохируулах уу?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g>-ыг таны үндсэн залгагч апп болгон тохируулах уу?"</string>
</resources>
diff --git a/res/values-mr-rIN/strings.xml b/res/values-mr-rIN/strings.xml
index c7c795c..6c9a25d 100644
--- a/res/values-mr-rIN/strings.xml
+++ b/res/values-mr-rIN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"फोन"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"फोन कॉल व्यवस्थापन"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"फोन"</string>
<string name="unknown" msgid="6878797917991465859">"अज्ञात"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"सुटलेला कॉल"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"कार्याचा कॉल चुकविला"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"सुटलेले कॉल"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> सुटलेले कॉल"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> कडील सुटलेला कॉल"</string>
@@ -26,20 +28,24 @@
<string name="notification_missedCall_message" msgid="3049928912736917988">"संदेश"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"कॉल नि.शब्द केला."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"स्पीकरफोन सक्षम केला."</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"आता बोलू शकत नाही. काय चालले आहे?"</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"आत्ता बोलू शकत नाही. काय चालले आहे?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"मी आपल्याला परत कॉल करेन."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"मी आपल्याला नंतर कॉल करेन."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"आता बोलू शकत नाही. मला नंतर कॉल कराल?"</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"आत्ता बोलू शकत नाही. नंतर कॉल करा?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"द्रुत प्रतिसाद"</string>
<string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"द्रुत प्रतिसाद संपादित करा"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"द्रुत प्रतिसाद"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"संदेश <xliff:g id="PHONE_NUMBER">%s</xliff:g> वर पाठविला."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"डिव्हाइस मालकाद्वारे केवळ आणीबाणी कॉलना अनुमती आहे"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"कॉल करण्याची खाती"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"फक्त आणीबाणी कॉल करण्याची परवानगी आहे."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"हा अनुप्रयोग फोन परवानगी शिवाय कॉल करू शकत नाही."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"कॉल करण्यासाठी, एक वैध नंबर प्रविष्ट करा."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"यावेळी कॉल जोडला जाऊ शकत नाही."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"कृपया व्हिडिओ कॉल करण्यासाठी TTY मोड अक्षम करा."</string>
<string name="no_vm_number" msgid="4164780423805688336">"व्हॉइसमेल नंबर गहाळ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"सिम कार्डवर कोणताही व्हॉइसमेल नंबर संचयित केला नाही."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"नंबर जोडा"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"डीफॉल्ट डायलर अॅप बदलायचा?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"आपला डीफॉल्ट डायलर अॅप म्हणून <xliff:g id="CURRENT_APP">%2$s</xliff:g> ऐवजी <xliff:g id="NEW_APP">%1$s</xliff:g> वापरायचा?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"आपला डीफॉल्ट डायलर अॅप म्हणून <xliff:g id="NEW_APP">%s</xliff:g> वापरायचा?"</string>
</resources>
diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml
index dc15ef3..0504627 100644
--- a/res/values-ms-rMY/strings.xml
+++ b/res/values-ms-rMY/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Pengurusan Panggilan Telefon"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Tidak diketahui"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Panggilan tidak dijawab"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Panggilan terlepas daripada tempat kerja"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Panggilan tidak dijawab"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> panggilan tidak dijawab"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Panggilan tidak dijawab daripada <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -26,20 +28,24 @@
<string name="notification_missedCall_message" msgid="3049928912736917988">"Mesej"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Panggilan diredam."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Telefon pembesar suara didayakan."</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Tak boleh berckp skrg. Apa cerita?"</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Sedang sibuk. Ada apa?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Saya akn segera hubungi awak nanti."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Saya akan hubungi awak kemudian."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Tdk blh berckp skg. Tel saya nanti?"</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Sedang sibuk. Telefon saya nanti?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Respons pantas"</string>
<string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Edit respons pantas"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Respons pantas"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mesej dihantar ke <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Hanya panggilan kecemasan dibenarkan oleh pemilik peranti"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Akaun panggilan"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Panggilan kecemasan sahaja dibenarkan."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Aplikasi ini tidak boleh membuat panggilan keluar tanpa kebenaran Telefon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Untuk membuat panggilan, masukkan nombor yang sah."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Panggilan tidak boleh ditambahkan pada masa ini."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Sila lumpuhkan Mod TTY untuk membuat panggilan video."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Nombor mel suara tiada"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Tidak ada nombor mel suara disimpan pada kad SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Tambah nombor"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Tukar apl Pendail lalai?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Gunakan <xliff:g id="NEW_APP">%1$s</xliff:g> dan bukannya <xliff:g id="CURRENT_APP">%2$s</xliff:g> sebagai apl pendail lalai anda?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Gunakan <xliff:g id="NEW_APP">%s</xliff:g> sebagai apl pendail lalai anda?"</string>
</resources>
diff --git a/res/values-my-rMM/strings.xml b/res/values-my-rMM/strings.xml
index 2bb5771..5db1d5b 100644
--- a/res/values-my-rMM/strings.xml
+++ b/res/values-my-rMM/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ဖုန်း"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ဖုန်းခေါ်ဆိုခြင်း စီမံမှု"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ဖုန်း"</string>
<string name="unknown" msgid="6878797917991465859">"အကြောင်းအရာ မသိရှိ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"လွဲသွားသော ဖုန်းခေါ်မှု"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"လွတ်သွားသည့် အလုပ်ဆိုင်ရာ ခေါ်ဆိုမှု"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"လွဲသွားသော ဖုန်းခေါ်မှုများ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> လွဲသွားသော ဖုန်းခေါ်မှုများ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> က ဖုန်းခေါ်မှုကို မကိုင်မိပါ"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"အမြန်တုံ့ပြန်ချက်"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> ထံ စာတိုပို့လိုက်ပါပြီ"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"အရေးပေါ်ဖုန်းများကိုသာ ခေါ်ဆိုနိုင်ရန် စက်ကိရိယာပိုင်ရှင်က ခွင့်ပြုထား၏"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"ခေါ်ဆိုသော အကောင့်များ"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"အရေးပေါ်ခေါ်ဆိုမှုများသာ ခွင့်ပြုပါသည်။"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ဤအပ္ပလီကေးရှင်းသည် ဖုန်းခွင့်ပြုချက်မရှိဘဲ အထွက်ခေါ်ဆိုမှု ပြုလုပ်၍မရပါ။"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ဖုန်းခေါ်ရန်အတွက်၊ သင့်လျော်သည့်နံပါတ် ရိုက်ထည့်ပါ။"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ဗွီဒီယိုခေါ်နေစဉ် ထပ်ခေါ်မရပါ။"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"ဗီဒီယိုခေါ်ဆိုမှုများ ပြုလုပ်ရန် ကျေးဇူးပြု၍ TTY မုဒ်ကို ပိတ်ထားပါ။"</string>
<string name="no_vm_number" msgid="4164780423805688336">"အသံစာပို့စနစ် နံပါတ် ပျောက်နေပါသည်"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"ဆင်းမ်ကဒ်ပေါ်တွင် အသံစာပို့စနစ် နံပါတ် သိမ်းဆည်ထားခြင်း မရှိပါ"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"နံပါတ်ထပ်ထည့်ရန်"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"စက်ရုံထုတ်ဖုန်းခေါ်အပ်ဖ်ကိုပြောင်းလိုပါသလား"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> အစား <xliff:g id="NEW_APP">%1$s</xliff:g> ကိုသင့်ရဲ့ စက်ရုံထုတ်ဖုန်းခေါ်အပ်ဖ်အဖြစ် သုံးလိုပါသလား"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> ကိုသင့်ရဲ့စက်ရုံထုတ်ဖုန်းခေါ်အပ်ဖ်အဖြစ် သုံးလိုပါသလား"</string>
</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index c28a01d..04588eb 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Administrering av telefonsamtaler"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Ukjent"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Tapt anrop"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Tapt jobbanrop"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Tapte anrop"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> tapte anrop"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Tapt anrop fra <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -27,19 +29,23 @@
<string name="accessibility_call_muted" msgid="2776111226185342220">"Samtalelyd er kuttet."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Høyttaler er aktivert."</string>
<string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Kan ikke snakke nå. Hva skjer?"</string>
- <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Jeg ringer deg straks tilbake."</string>
+ <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Jeg ringer deg tilbake straks."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Jeg ringer deg senere."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Ikke nå. Ringer du meg senere?"</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Kan ikke nå. Ring meg senere."</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Hurtigsvar"</string>
<string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Rediger hurtigsvar"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Hurtigsvar"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Melding er sendt til <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Eieren av enheten tillater bare nødanrop"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Ringekontoer"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Kun nødanrop er mulig."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Denne appen kan ikke ringe uten tillatelse fra telefonen."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Skriv inn et gyldig nummer for å plassere en samtale."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Anrop kan ikke legges til akkurat nå."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Slå av TTY-modus for å starte videosamtaler."</string>
- <string name="no_vm_number" msgid="4164780423805688336">"Mangler nummer til telefonsvarer"</string>
- <string name="no_vm_number_msg" msgid="1300729501030053828">"Det er ikke lagret noe telefonsvarernummer på SIM-kortet."</string>
+ <string name="no_vm_number" msgid="4164780423805688336">"Mangler nummer til talepostkasse"</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"Det er ikke lagret noe nummer for talepostkasse på SIM-kortet."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Legg til nummer"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Vil du endre standard telefonapp?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Vil du bruke <xliff:g id="NEW_APP">%1$s</xliff:g> i stedet for <xliff:g id="CURRENT_APP">%2$s</xliff:g> som standard telefonapp?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Vil du bruke <xliff:g id="NEW_APP">%s</xliff:g> som standard telefonapp?"</string>
</resources>
diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne-rNP/strings.xml
index 1cac5f6..a804221 100644
--- a/res/values-ne-rNP/strings.xml
+++ b/res/values-ne-rNP/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"फोन"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"फोन कल व्यवस्थापन"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"फोन"</string>
<string name="unknown" msgid="6878797917991465859">"अज्ञात"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"छुटेका कल"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"छुटेको कार्यको कल"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"छुटेका कल"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> छुटेका कलहरू"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>बाट छुटेका कल"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"द्रुत प्रतिक्रिया"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> लाई सन्देश पठाइयो।"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"केवल आपतकालीन कलहरू मात्र यन्त्र मालिकद्वारा अनुमति दिइएको छ"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"कलिङ खाताहरू"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"आपतकालीन कलहरूलाई मात्र अनुमति दिइएको छ।"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"यो अनुप्रयोगले फोनको अनुमति बिना बहिर्गमन कलहरू गर्न सक्दैन।"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"एक कल गर्नको लागि, एक वैध नम्बर प्रविष्ट गर्नुहोस्।"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"यस समयमा कल थप गर्न सकिँदैन।"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"कृपया भिडियो कलहरू गर्न TTY मोड निष्क्रिय गर्नुहोस्।"</string>
<string name="no_vm_number" msgid="4164780423805688336">"भ्वाइसमेल नम्बर हराइरहेको छ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM कार्डमा कुनै पनि भ्वाइसमेल नम्बर भण्डारण भएको छैन।"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"नम्बर थप्नुहोस्"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"पूर्वनिर्धारित डायलर अनुप्रयोग फेर्ने हो?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g>को सट्टामा <xliff:g id="NEW_APP">%1$s</xliff:g>लाई पूर्वनिर्धारित डायलर अनुप्रयोगको रूपमा प्रयोग गर्ने हो?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g>लाई तपाईँको पूर्वनिर्धारित डायलर अनुप्रयोगको रूपमा प्रयोग गर्ने हो?"</string>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index b762e9a..829bf0b 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefoon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefoonoproepbeheer"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefoon"</string>
<string name="unknown" msgid="6878797917991465859">"Onbekend"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Gemiste oproep"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Gemiste zakelijke oproep"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Gemiste oproepen"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> gemiste oproepen"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Gemiste oproep van <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Snelle reactie"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Bericht verzonden naar <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Alleen noodoproepen zijn toegestaan door de apparaateigenaar"</string>
- <string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Als u wilt bellen, moet u een geldig nummer invoeren."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Oproepaccounts"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Alleen noodoproepen zijn toegestaan."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Deze app kan geen uitgaande oproepen starten zonder telefoonrechten."</string>
+ <string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Als je wilt bellen, moet je een geldig nummer invoeren."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Oproep kan momenteel niet worden toegevoegd."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Schakel de TTY-modus uit om videogesprekken te voeren."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Voicemailnummer ontbreekt"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Er is geen voicemailnummer op de SIM-kaart opgeslagen."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Nummer toevoegen"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Standaard kiezerapp wijzigen?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g> in plaats van <xliff:g id="CURRENT_APP">%2$s</xliff:g> gebruiken als standaard kiezerapp?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> gebruiken als standaard kiezerapp?"</string>
</resources>
diff --git a/res/values-pa-rIN/strings.xml b/res/values-pa-rIN/strings.xml
index 9e5cf0c..41e1a4a 100644
--- a/res/values-pa-rIN/strings.xml
+++ b/res/values-pa-rIN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ਫੋਨ"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ਫ਼ੋਨ - ਕਾਲ ਪ੍ਰਬੰਧਨ"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ਫ਼ੋਨ"</string>
<string name="unknown" msgid="6878797917991465859">"ਅਗਿਆਤ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"ਮਿਸਡ ਕਾਲ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"ਕੰਮ ਨਾਲ ਸਬੰਧਿਤ ਖੁੰਝੀ ਕਾਲ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"ਮਿਸਡ ਕਾਲਾਂ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ਮਿਸਡ ਕਾਲਾਂ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> ਵੱਲੋਂ ਮਿਸਡ ਕਾਲ"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ਤਤਕਾਲ ਜਵਾਬ"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"ਸੁਨੇਹਾ <xliff:g id="PHONE_NUMBER">%s</xliff:g> ਨੂੰ ਭੇਜਿਆ ਗਿਆ।"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"ਡਿਵਾਈਸ ਦੇ ਮਾਲਕ ਵੱਲੋਂ ਕੇਵਲ ਐਮਰਜੈਂਸੀ ਕਾਲਾਂ ਦੀ ਆਗਿਆ ਹੈ।"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"ਕਾਲਿੰਗ ਖਾਤੇ"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"ਸਿਰਫ਼ ਸੰਕਟ ਕਾਲਾਂ ਨੂੰ ਮਨਜ਼ੂਰੀ ਹੈ।"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ਇਹ ਐਪਲੀਕੇਸ਼ਨ ਫੋਨ ਅਨੁਮਤੀ ਦੇ ਬਿਨਾਂ ਆਉਟਗੋਇੰਗ ਕਾਲਾਂ ਨਹੀਂ ਕਰ ਸਕਦੀ।"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ਇੱਕ ਕਾਲ ਕਰਨ ਲਈ, ਇੱਕ ਪ੍ਰਮਾਣਿਕ ਨੰਬਰ ਦਰਜ ਕਰੋ।"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ਇਸ ਵੇਲੇ ਕਾਲ ਨਹੀਂ ਜੋੜੀ ਸਕਦੀ।"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"ਕਿਰਪਾ ਕਰਕੇ ਵੀਡੀਓ ਕਾਲਾਂ ਨੂੰ ਕਰਨ ਲਈ TTY ਮੋਡ ਅਸਮਰਥਿਤ ਕਰੋ."</string>
<string name="no_vm_number" msgid="4164780423805688336">"ਲੁਪਤ ਵੌਇਸਮੇਲ ਨੰਬਰ"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM ਕਾਰਡ ਤੇ ਕੋਈ ਵੌਇਸਮੇਲ ਨੰਬਰ ਸਟੋਰ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹੈ।"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"ਨੰਬਰ ਜੋੜੋ"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ਕੀ ਡਿਫ਼ੌਲਟ ਡਾਇਲਰ ਐਪ ਨੂੰ ਬਦਲਣਾ ਹੈ?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"ਕੀ <xliff:g id="CURRENT_APP">%2$s</xliff:g> ਦੀ ਬਜਾਏ <xliff:g id="NEW_APP">%1$s</xliff:g> ਨੂੰ ਆਪਣੀ ਡਿਫ਼ੌਲਟ ਡਾਇਲਰ ਐਪ ਦਾ ਉਪਯੋਗ ਕਰਨਾ ਹੈ?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"ਕੀ <xliff:g id="NEW_APP">%s</xliff:g> ਨੂੰ ਆਪਣੀ ਡਿਫ਼ੌਲਟ ਡਾਇਲਰ ਐਪ ਦਾ ਉਪਯੋਗ ਕਰਨਾ ਹੈ?"</string>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 515190c..813bf29 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefon – zarządzanie połączeniami"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Nieznany"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Nieodebrane połączenie"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Nieodebrane połączenie (praca)"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Połączenia nieodebrane"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> nieodebranych połączeń"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Nieodebrane połączenie z <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Szybka odpowiedź"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Wiadomość wysłano na numer <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Właściciel urządzenia zezwala tylko na połączenia alarmowe"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Konta telefoniczne"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Dozwolone są tylko połączenia alarmowe."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ta aplikacja nie może wykonywać połączeń bez uprawnienia Telefon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Aby zadzwonić, wybierz prawidłowy numer."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Nie można w tej chwili dodać połączenia."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Wyłącz tryb TTY, by korzystać z rozmów wideo."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Brakuje numeru poczty głosowej"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Na karcie SIM nie ma zapisanego numeru poczty głosowej."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Dodaj numer"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Zmienić domyślną aplikację telefonu?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Zmienić <xliff:g id="CURRENT_APP">%2$s</xliff:g> na <xliff:g id="NEW_APP">%1$s</xliff:g> jako domyślną aplikację telefonu?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Ustawić <xliff:g id="NEW_APP">%s</xliff:g> jako domyślną aplikację telefonu?"</string>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index d16d6d1..cc3db6e 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telemóvel"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Gestão de chamadas do telemóvel"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefone"</string>
<string name="unknown" msgid="6878797917991465859">"Desconhecido"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Chamada não atendida"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Chamada de trabalho não atendida"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Chamadas não atendidas"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> chamadas não atendidas"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Chamada não atendida de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Resposta rápida"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mensagem enviada para <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Só são permitidas chamadas de emergência pelo proprietário do dispositivo"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Contas de chamadas"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Apenas são permitidas chamadas de emergência."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Esta aplicação não pode fazer chamadas sem a autorização do telefone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Para telefonar, introduza um número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Não é possível adicionar a chamada neste momento."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Desative o modo teletipo para efetuar videochamadas."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Número do correio de voz em falta"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Não existe um número de correio de voz armazenado no cartão SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Adicionar número"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Alterar aplicação de Telefone predefinida?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Utilizar <xliff:g id="NEW_APP">%1$s</xliff:g> em vez de <xliff:g id="CURRENT_APP">%2$s</xliff:g> como a aplicação de telefone predefinida?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Utilizar <xliff:g id="NEW_APP">%s</xliff:g> como a aplicação de telefone predefinida?"</string>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 0c5f2dc..9dea13f 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefone"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Gerenciamento de chamadas telefônicas"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefone"</string>
<string name="unknown" msgid="6878797917991465859">"Desconhecido"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Chamada perdida"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Chamada de trabalho perdida"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Chamadas perdidas"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> chamadas perdidas"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Chamada perdida de <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Resposta rápida"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mensagem enviada para <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Apenas chamadas de emergência são permitidas pelo proprietário do dispositivo"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Contas de chamadas"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Somente chamadas de emergência são permitidas."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Este aplicativo não pode fazer chamadas sem a permissão do smartphone."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Para realizar uma chamada, digite um número válido."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"No momento, não é possível adicionar a chamada."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Desative o modo TTD para realizar vídeo chamadas."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Número correio de voz ausente"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Não há um número correio de voz armazenado no cartão SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Adicionar número"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Alterar o app discador padrão?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Usar <xliff:g id="NEW_APP">%1$s</xliff:g> em vez de <xliff:g id="CURRENT_APP">%2$s</xliff:g> como seu app discador padrão?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Usar <xliff:g id="NEW_APP">%s</xliff:g> como seu app discador padrão?"</string>
</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 090451e..fd7050d 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -16,13 +16,15 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefon – Gestionarea apelurilor"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Necunoscut"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Apel nepreluat"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Apel de serviciu nepreluat"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Apeluri nepreluate"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> (de) apeluri nepreluate"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Apel nepreluat de la <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Sunaţi înapoi"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Sunați"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Mesaj"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Apel cu sunet dezactivat."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Difuzor activat."</string>
@@ -31,15 +33,19 @@
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Te sun mai târziu."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Nu pot acum. Vorbim mai târziu?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Răspunsuri rapide"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Editaţi răspunsurile rapide"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Editați răspunsurile rapide"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Răspuns rapid"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mesajul a fost trimis la <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Proprietarul dispozitivului permite numai apelurile de urgență"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Conturi pentru apelare"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Sunt permise doar apelurile de urgență."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Această aplicație nu poate efectua apeluri fără permisiunea Telefon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Pentru a apela, introduceți un număr valid."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Apelul nu poate fi adăugat în acest moment."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Dezactivați modul TTY pentru a iniția apeluri video."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Lipseşte numărul mesageriei vocale"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Niciun număr de mesagerie vocală nu este stocat pe cardul SIM."</string>
- <string name="add_vm_number_str" msgid="4676479471644687453">"Adăugaţi numărul"</string>
+ <string name="add_vm_number_str" msgid="4676479471644687453">"Adăugați numărul"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Schimbați aplicația Telefon prestabilită?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Folosiți <xliff:g id="NEW_APP">%1$s</xliff:g> și nu <xliff:g id="CURRENT_APP">%2$s</xliff:g> ca aplicație de telefonie prestabilită?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Folosiți <xliff:g id="NEW_APP">%s</xliff:g> ca aplicație de telefonie prestabilită?"</string>
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 0145c3b..54d5711 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Телефон"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Телефон – управление звонками"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
<string name="unknown" msgid="6878797917991465859">"Неизвестный абонент"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Пропущенный вызов"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Пропущенный звонок (работа)"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Пропущенные вызовы"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"Пропущенных вызовов: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Пропущенные вызовы от абонента <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Быстрый ответ"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Сообщение отправлено на номер <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Владелец устройства разрешил только экстренные вызовы"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Аккаунты для звонков"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Только экстренные вызовы"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Приложение не может совершать звонки без соответствующего разрешения."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Недействительный номер."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Невозможно позвонить в данный момент"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Чтобы делать видеозвонки, отключите режим телетайпа"</string>
<string name="no_vm_number" msgid="4164780423805688336">"Не указан номер голосовой почты"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"На SIM-карте нет ни одного номера голосовой почты."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Добавить номер"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Новое приложение для звонков"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"\"<xliff:g id="NEW_APP">%1$s</xliff:g>\" станет приложением для звонков по умолчанию вместо \"<xliff:g id="CURRENT_APP">%2$s</xliff:g>\". Продолжить?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"\"<xliff:g id="NEW_APP">%s</xliff:g>\" станет приложением для звонков по умолчанию. Продолжить?"</string>
</resources>
diff --git a/res/values-si-rLK/strings.xml b/res/values-si-rLK/strings.xml
index b8f7bc4..f1b014c 100644
--- a/res/values-si-rLK/strings.xml
+++ b/res/values-si-rLK/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"දුරකථනය"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"දුරකථන ඇමතුම් කළමනාකරණය"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"දුරකථනය"</string>
<string name="unknown" msgid="6878797917991465859">"නොදනී"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"මඟ හැරුණු ඇමතුම"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"මග හැරුණ කාර්යාල ඇමතුම"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"මඟ හැරුණු ඇමතුම්"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"මඟ හැරුණු ඇමතුම් <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> වෙතින් මඟ හැරුණු ඇමතුම්"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"ක්ෂණික ප්රතිචාරය"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> හට පණිවිඩය යවන්න."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"උපාංගයේ හිමිකරු විසින් හදිස්සි ඇමතුම් වලට පමණක් අවසර දෙයි"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"ඇමතීමේ ගිණුම්"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"හදිසි ඇමතුම්වලට පමණක් ඉඩ දේ."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"දුරකථන අවසරයෙන් තොරව මෙම යෙදුමට පිටතට යන ඇමතුම් සිදු කළ නොහැකිය."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"ඇමතුමක් ලබාගැනීමට, වලංගු අංකයක් ලබාගන්න."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"මේ වේලාවේ ඇමතුම එකතු කළ නොහැක."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"වීඩියෝ ඇමතුම් ලබා ගැනීමට කරුණාකර TTY මෝඩය අබල කරන්න."</string>
<string name="no_vm_number" msgid="4164780423805688336">"හඬ තැපැල් අංකය නැත"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM කාඩ් පතෙහි හඬ තැපැල් අංකයක් ආචිත වී නැත."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"අංකයක් එක් කරන්න"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"සුපුරුදු දුරකථන යෙදුම වෙනස් කරන්න ද?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> වෙනුවට <xliff:g id="NEW_APP">%1$s</xliff:g> ඔබගේ සුපුරුදු දුරකථන යෙදුම ලෙස භාවිතා කරන්නද?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> ඔබගේ සුපුරුදු දුරකථන යෙදුම ලෙස භාවිතා කරන්නද?"</string>
</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 030d517..9139650 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -16,14 +16,16 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefón"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Správa telefónnych hovorov"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefón"</string>
<string name="unknown" msgid="6878797917991465859">"Neznáme"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Zmeškaný hovor"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Zmeškaný pracovný hovor"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Zmeškané hovory"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"Zmeškané hovory: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>."</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Zmeškaný hovor od volajúceho <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Zavolať späť"</string>
- <string name="notification_missedCall_message" msgid="3049928912736917988">"Správa"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Zavolať"</string>
+ <string name="notification_missedCall_message" msgid="3049928912736917988">"Napísať"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Zvuk hovoru bol vypnutý."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Reproduktor je povolený."</string>
<string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Teraz nemôžem hovoriť, o čo ide?"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Rýchla odpoveď"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Správa bola odoslaná na číslo <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Vlastník zariadenia povolil iba tiesňové volania."</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Telefónne účty"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Povolené sú len tiesňové volania."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"V tejto aplikácii nie je možné uskutočňovať odchádzajúce hovory bez povolenia Telefón"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Ak chcete volať, zadajte platné číslo"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Hovor momentálne nie je možné pridať."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Ak chcete uskutočňovať videohovory, deaktivujte režim TTY."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Chýba číslo hlasovej schránky"</string>
- <string name="no_vm_number_msg" msgid="1300729501030053828">"Na karte SIM nie je uložené žiadne číslo hlasovej schránky."</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"Na SIM karte nie je uložené žiadne číslo hlasovej schránky."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Pridať číslo"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Chcete zmeniť predvolenú aplikáciu vytáčania?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Chcete použiť aplikáciu <xliff:g id="NEW_APP">%1$s</xliff:g> namiesto aplikácie <xliff:g id="CURRENT_APP">%2$s</xliff:g> ako predvolenú aplikáciu vytáčania?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Chcete použiť aplikáciu <xliff:g id="NEW_APP">%s</xliff:g> ako predvolenú aplikáciu vytáčania?"</string>
</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 51f9de5..374266e 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Upravljanje telefonskih klicev"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Neznano"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Neodgovorjeni klic"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Zgrešen delovni klic"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Neodgovorjeni klici"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> neodgovorjenih klicev"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Neodgovorjeni klic od <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Hiter odgovor"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"SMS poslan na številko <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Lastnik naprave dovoljuje samo opravljanje klicev v sili"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Računi za klicanje"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Dovoljeno je samo opravljanje klicev v sili."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ta aplikacija lahko opravlja odhodne klice brez dovoljenja za telefon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Če želite opraviti klic, vnesite veljavno številko."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Klica trenutno ni mogoče dodati."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Če želite opravljati videoklice, onemogočite način TTY."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Manjkajoča številka glasovne pošte"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Na kartici SIM ni shranjena številka glasovne pošte."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Dodaj številko"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Želite spremeniti privzeto aplikacijo za klicanje?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Želite <xliff:g id="NEW_APP">%1$s</xliff:g> uporabljati kot privzeto aplikacijo za klicanje namesto <xliff:g id="CURRENT_APP">%2$s</xliff:g>?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Želite <xliff:g id="NEW_APP">%s</xliff:g> uporabljati kot privzeto aplikacijo za klicanje?"</string>
</resources>
diff --git a/res/values-sq-rAL/strings.xml b/res/values-sq-rAL/strings.xml
index 75361a8..b328b7f 100644
--- a/res/values-sq-rAL/strings.xml
+++ b/res/values-sq-rAL/strings.xml
@@ -16,13 +16,15 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefoni"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Menaxhimi i telefonatave"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefoni"</string>
<string name="unknown" msgid="6878797917991465859">"I panjohur"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Telefonatë e humbur"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Telefonatë pune e humbur"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Telefonata të humbura"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> telefonata të humbura"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Telefonatë e humbur nga <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Ri-telefono"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Telefono"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"Mesazh"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Telefonata kaloi në heshtje."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Altoparlanti u aktivizua."</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Përgjigje e shpejtë"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mesazhi u dërgua te <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Vetëm telefonatat e urgjencës lejohen nga zotëruesi i pajisjes"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Llogaritë e telefonatave"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Lejohen vetëm telefonatat e urgjencës."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ky aplikacion nuk mund të kryejë telefonata dalëse pa lejen e Telefonit."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Për të kryer një telefonatë, fut një numër të vlefshëm."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Telefonata nuk mund të shtohet këtë herë."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Çaktivizo modalitetin TTY për të kryer telefonata me video."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Mungon numri i postës zanore"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Nuk ka numër të ruajtur në kartën SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Shto numër"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Të ndryshohet apl. parazgjedhur i formuesit të numrave?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Të përdoret <xliff:g id="NEW_APP">%1$s</xliff:g> në vend të <xliff:g id="CURRENT_APP">%2$s</xliff:g> si aplikacioni i parazgjedhur i formuesit të numrave?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Të përdoret <xliff:g id="NEW_APP">%s</xliff:g> si aplikacioni i parazgjedhur i formuesit të numrave?"</string>
</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 36d967c..2eaadc5 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Телефон"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Управљање телефонским позивима"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
<string name="unknown" msgid="6878797917991465859">"Непознато"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Пропуштен позив"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Пропуштен позив за Work"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Пропуштени позиви"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"Број пропуштених позива: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Пропуштен позив од: <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -26,7 +28,7 @@
<string name="notification_missedCall_message" msgid="3049928912736917988">"Порука"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Звук позива је искључен."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Спикерфон је омогућен."</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"У гужви сам. Шта је у питању?"</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"У гужви сам. О чему се ради?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Позваћу те ускоро."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Позваћу те касније."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"У гужви сам. Да се чујемо касније?"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Брзи одговор"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Порука је послата на број <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Власник уређаја је дозволио само хитне позиве"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Налози за позивање"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Дозвољени су само хитни позиви."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ова апликација не може да позива без дозволе за телефонирање."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Да бисте упутили позив, унесите важећи број."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Тренутно није могуће додати позив."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Онемогућите TTY режим да бисте упућивали видео позиве."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Недостаје број за говорну пошту"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Није ускладиштен ниједан број говорне поште на SIM картици."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Додај број"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Променити подразумевану апликацију Телефон?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Желите ли да користите апликацију <xliff:g id="NEW_APP">%1$s</xliff:g> уместо апликације <xliff:g id="CURRENT_APP">%2$s</xliff:g> као подразумевану апликацију за позивање телефонских бројева?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Желите ли да користите апликацију <xliff:g id="NEW_APP">%s</xliff:g> као подразумевану апликацију за позивање телефонских бројева?"</string>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index a2fa42d..ddd421c 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -16,14 +16,16 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Samtalshantering"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Okänd"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Missat samtal"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Missat jobbsamtal"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Missade samtal"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> missade samtal"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Missat samtal från <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
<string name="notification_missedCall_call_back" msgid="2684890353590890187">"Ring upp"</string>
- <string name="notification_missedCall_message" msgid="3049928912736917988">"Meddelande"</string>
+ <string name="notification_missedCall_message" msgid="3049928912736917988">"Sms:a"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Samtalets ljud avstängt."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Högtalartelefon aktiverad."</string>
<string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Kan inte prata nu. Läget?"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Snabbsvar"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Meddelandet har skickats till <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Endast nödsamtal är tillåtna av enhetens ägare"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Konton för samtal"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Det går bara att ringa nödsamtal."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Den här appen kan inte göra utgående samtal utan behörigheten Telefon."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Ange ett giltigt nummer om du vill ringa ett samtal."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Det går inte att lägga till samtalet just nu."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Inaktivera texttelefonläget om du vill ringa videosamtal."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Nummer till röstbrevlåda saknas"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Det finns inget nummer till röstbrevlådan sparat på SIM-kortet."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Lägg till nummer"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Vill du byta standardapp för uppringning?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Vill du använda <xliff:g id="NEW_APP">%1$s</xliff:g> i stället för <xliff:g id="CURRENT_APP">%2$s</xliff:g> som standardapp för uppringning?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Vill du använda <xliff:g id="NEW_APP">%s</xliff:g> som standardapp för uppringning?"</string>
</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 9e71c93..4686e15 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Nambari ya simu"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Usimamizi wa Mazunguzo ya Simu"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Simu"</string>
<string name="unknown" msgid="6878797917991465859">"Haijulikani"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Simu isiyojibiwa"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Simu ya kazini ambayo hukujibu"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Simu zisizojibiwa"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> simu ambazo hazijajibiwa"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Simu isiyojibiwa kutoka <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Majibu ya haraka"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Ujumbe uliotumwa kwa <xliff:g id="PHONE_NUMBER">%s</xliff:g> ."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Ni simu za dharura pekee zinazoruhusiwa na mmiliki wa kifaa"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Akaunti za simu"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Piga simu za dharura pekee."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Programu hii haiwezi kupiga simu bila ruhusa ya Simu."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Ili upige simu, weka nambari sahihi."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Hangout ya video haiwezi kuongezwa kwa wakati huu."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Tafadhali zima Hali ya TTY ili uanzishe Hangout ya video."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Nambari ya sauti inayokosekana"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Hakuna nambari ya ujumbe wa sauti iliyohifadhiwa katika SIM kadi."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Ongeza nambari"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Ungependa kubadilisha programu chaguo-msingi ya simu?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Ungependa kutumia <xliff:g id="NEW_APP">%1$s</xliff:g> badala ya <xliff:g id="CURRENT_APP">%2$s</xliff:g> kama programu ya chaguo-msingi ya kupigia simu?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Ungependa kutumia <xliff:g id="NEW_APP">%s</xliff:g> kama programu ya chaguo-msingi ya kupigia simu?"</string>
</resources>
diff --git a/res/values-ta-rIN/strings.xml b/res/values-ta-rIN/strings.xml
index 190cd0c..d22d047 100644
--- a/res/values-ta-rIN/strings.xml
+++ b/res/values-ta-rIN/strings.xml
@@ -16,13 +16,15 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ஃபோன்"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ஃபோன் அழைப்பு நிர்வாகம்"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ஃபோன்"</string>
<string name="unknown" msgid="6878797917991465859">"தெரியாதவர்"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"தவறிய அழைப்பு"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"தவறிய அழைப்பு (பணி)"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"தவறிய அழைப்புகள்"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> தவறிய அழைப்புகள்"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> இடமிருந்து தவறிய அழைப்பு"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"திருப்பி அழை"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"அழை"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"செய்தி"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"அழைப்பு முடக்கப்பட்டது."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"ஸ்பீக்கர்ஃபோன் இயக்கப்பட்டது."</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"விரைவு பதில்"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> க்குச் செய்தி அனுப்பப்பட்டது."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"சாதன உரிமையாளர் அவசர அழைப்புகளை மட்டுமே அனுமதித்துள்ளார்"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"அழைப்புக் கணக்குகள்"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"அவசர அழைப்புகள் மட்டுமே அனுமதிக்கப்படும்."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ஃபோன் அனுமதியில்லாமல், பயன்பாட்டினால் வெளிச்செல்லும் அழைப்புகளைச் செய்ய முடியாது."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"அழைக்க, சரியான எண்ணை உள்ளிடவும்."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"இப்போது அழைக்க முடியாது."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"வீடியோ அழைப்புகளை மேற்கொள்ள, TTY பயன்முறையை முடக்கவும்."</string>
<string name="no_vm_number" msgid="4164780423805688336">"குரலஞ்சல் எண் இல்லை"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"சிம் கார்டில் குரலஞ்சலுக்கான எண் எதுவும் சேமிக்கப்படவில்லை."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"எண்ணைச் சேர்"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"இயல்புநிலை டயலர் பயன்பாட்டை மாற்றவா?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="NEW_APP">%1$s</xliff:g>ஐ <xliff:g id="CURRENT_APP">%2$s</xliff:g>க்குப் பதிலாக, இயல்புநிலை டயலர் பயன்பாடாகப் பயன்படுத்தவா?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g>ஐ இயல்புநிலை டயலர் பயன்பாடாகப் பயன்படுத்தவா?"</string>
</resources>
diff --git a/res/values-te-rIN/strings.xml b/res/values-te-rIN/strings.xml
index ee0c5b5..83d2074 100644
--- a/res/values-te-rIN/strings.xml
+++ b/res/values-te-rIN/strings.xml
@@ -16,13 +16,15 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"ఫోన్"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"ఫోన్ కాల్ నిర్వహణ"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"ఫోన్"</string>
<string name="unknown" msgid="6878797917991465859">"తెలియదు"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"సమాధానం ఇవ్వని కాల్"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"మిస్డ్ కార్యాలయ కాల్"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"సమాధానం ఇవ్వని కాల్లు"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> సమాధానం ఇవ్వని కాల్లు"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> నుండి సమాధానం ఇవ్వని కాల్"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"తిరిగి కాల్ చేయి"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"కాల్ చేయి"</string>
<string name="notification_missedCall_message" msgid="3049928912736917988">"సందేశం"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"కాల్ మ్యూట్ చేయబడింది."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"స్పీకర్ ఫోన్ ప్రారంభించబడింది."</string>
@@ -31,15 +33,19 @@
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"నేను మీకు తర్వాత కాల్ చేస్తాను."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"ఇప్పుడు మాట్లాడలేను. నాకు తర్వాత కాల్ చేస్తారా?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"శీఘ్ర ప్రతిస్పందనలు"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"శీఘ్ర ప్రతిస్పందనలు సవరించండి"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"శీఘ్ర ప్రతిస్పందనల సవరణ"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"శీఘ్ర ప్రతిస్పందన"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"<xliff:g id="PHONE_NUMBER">%s</xliff:g>కు సందేశం పంపబడింది."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"పరికరం యజమాని అత్యవసర కాల్లను మాత్రమే అనుమతించారు"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"కాలింగ్ ఖాతాలు"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"కేవలం అత్యవసర కాల్లు మాత్రమే అనుమతించబడతాయి."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"ఈ అనువర్తనం ఫోన్ అనుమతి లేకుండా అవుట్గోయింగ్ కాల్లను చేయలేదు."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"కాల్ చేయడానికి, చెల్లుబాటు అయ్యే నంబర్ను నమోదు చేయండి."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ఈ సమయంలో కాల్ను జోడించడం సాధ్యపడదు."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"దయచేసి వీడియో కాల్లు చేయడానికి TTY మోడ్ నిలిపివేయండి."</string>
<string name="no_vm_number" msgid="4164780423805688336">"వాయిస్ మెయిల్ నంబర్ లేదు"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"సిమ్ కార్డులో వాయిస్ మెయిల్ నంబర్ ఏదీ నిల్వ చేయబడలేదు."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"నంబర్ను జోడించు"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"డిఫాల్ట్ డయలర్ అనువర్తనాన్ని మార్చాలా?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"మీ డిఫాల్ట్ డయలర్ అనువర్తనంగా <xliff:g id="CURRENT_APP">%2$s</xliff:g> బదులు <xliff:g id="NEW_APP">%1$s</xliff:g>ని ఉపయోగించాలా?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"మీ డిఫాల్ట్ డయలర్ అనువర్తనంగా <xliff:g id="NEW_APP">%s</xliff:g>ని ఉపయోగించాలా?"</string>
</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 2babae6..ee9b6fd 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"โทรศัพท์"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"การจัดการการโทรศัพท์"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"โทรศัพท์"</string>
<string name="unknown" msgid="6878797917991465859">"ไม่ทราบ"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"สายที่ไม่ได้รับ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"มีสายจากที่ทำงานที่ไม่ได้รับ"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"สายที่ไม่ได้รับ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> สายที่ไม่ได้รับ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"สายที่ไม่ได้รับจาก <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"คำตอบด่วน"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"ส่งข้อความไปยัง <xliff:g id="PHONE_NUMBER">%s</xliff:g> แล้ว"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"เจ้าของอุปกรณ์อนุญาตเฉพาะหมายเลขฉุกเฉิน"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"บัญชีการโทร"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"อนุญาตเฉพาะหมายเลขฉุกเฉิน"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"แอปพลิเคชันนี้ไม่สามารถโทรออกโดยไม่มีสิทธิ์ใช้โทรศัพท์"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"หากต้องการโทรออก โปรดป้อนหมายเลขที่ถูกต้อง"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"ไม่สามารถเพิ่มสายได้ในขณะนี้"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"โปรดปิดใช้โหมด TTY เพื่อแฮงเอาท์วิดีโอ"</string>
<string name="no_vm_number" msgid="4164780423805688336">"ไม่มีหมายเลขข้อความเสียง"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"ไม่มีหมายเลขข้อความเสียงจัดเก็บอยู่ในซิมการ์ด"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"เพิ่มหมายเลข"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"เปลี่ยนแอปแป้นโทรศัพท์เริ่มต้นไหม"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"ใช้ <xliff:g id="NEW_APP">%1$s</xliff:g> เป็นแอปแป้นโทรศัพท์เริ่มต้นแทน <xliff:g id="CURRENT_APP">%2$s</xliff:g> ไหม"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"ใช้ <xliff:g id="NEW_APP">%s</xliff:g> เป็นแอปแป้นโทรศัพท์เริ่มต้นไหม"</string>
</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index ca749b4..2de1ac5 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telepono"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Pamamahala sa Tawag sa Telepono"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telepono"</string>
<string name="unknown" msgid="6878797917991465859">"Di-kilala"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Hindi nasagot na tawag"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Hindi nasagot na tawag sa trabaho"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Mga hindi nasagot na tawag"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> (na) hindi nasagot na tawag"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Hindi nasagot na tawag mula kay <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -26,20 +28,24 @@
<string name="notification_missedCall_message" msgid="3049928912736917988">"Padalhan ng mensahe"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Naka-mute ang tawag."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Pinapagana ang speakerphone."</string>
- <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Di masagot ngayon. Ano meron?"</string>
+ <string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Di masagot ngayon. Ano\'ng meron?"</string>
<string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Tawagan kita ulit."</string>
<string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Tawagan kita mamaya."</string>
<string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Di masagot ngayon. Tawag ka mamaya?"</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Mga mabilisang tugon"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"I-edit ang mga mabilisang tugon"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"I-edit ang mga quick response"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Mabilisang tugon"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Naipadala ang mensahe sa <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Ang mga emergency na tawag lang ang pinapayagan ng may-ari ng device"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Account sa pagtawag"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Ang mga pang-emergency na tawag lang ang pinapayagan."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Hindi makakapagsagawa ng mga papalabas na tawag ang application na ito nang wala ang pahintulot ng Telepono."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Upang tumawag, maglagay ng wastong numero."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Hindi maidadagdag ang tawag sa oras na ito."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Paki-disable ang TTY Mode upang makapag-video call."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Nawawala ang numero ng voicemail"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Walang nakaimbak na numero ng voicemail sa SIM card."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Magdagdag ng numero"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Baguhin ang iyong default na Dialer app?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Gamitin ang <xliff:g id="NEW_APP">%1$s</xliff:g> sa halip na <xliff:g id="CURRENT_APP">%2$s</xliff:g> bilang default na dialer app mo?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Gamitin ang <xliff:g id="NEW_APP">%s</xliff:g> bilang default na dialer app mo?"</string>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 662c3fd..b3c6698 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefon Çağrısı Yönetimi"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Bilinmiyor"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Cevapsız çağrı"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"İşle ilgili cevapsız çağrı"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Cevapsız çağrılar"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> cevapsız çağrı"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Cevapsız çağrı: <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Hızlı yanıt"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Mesaj, <xliff:g id="PHONE_NUMBER">%s</xliff:g> numaralı telefona gönderildi."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Cihaz sahibi sadece acil durum çağrılarına izin veriyor"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Çağrı hesapları"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Yalnızca acil durum çağrılarına izin veriliyor."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Bu uygulama, Telefonun izni olmadan giden çağrılar yapamaz."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Telefon etmek için geçerli bir numara girin."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Çağrı şu anda eklenemiyor."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Video görüşmesi yapmak için lütfen TTY Modu\'nu devre dışı bırakın."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Eksik sesli mesaj numarası"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM kartta depolanan sesli mesaj numarası yok."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Numara ekle"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Varsayılan Numara Çeviri uygulaması değiştirilsin mi?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Varsayılan numara çevirici uygulamanız olarak <xliff:g id="CURRENT_APP">%2$s</xliff:g> yerine <xliff:g id="NEW_APP">%1$s</xliff:g> kullanılsın mı?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Varsayılan numara çevirici uygulamanız olarak <xliff:g id="NEW_APP">%s</xliff:g> kullanılsın mı?"</string>
</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 16214b4..7bdfe8c 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Номер телефону"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Керування дзвінками"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Телефон"</string>
<string name="unknown" msgid="6878797917991465859">"Невідомий"</string>
- <string name="notification_missedCallTitle" msgid="7554385905572364535">"Пропущ. виклик"</string>
+ <string name="notification_missedCallTitle" msgid="7554385905572364535">"Пропущений виклик"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Пропущений дзвінок на робочий телефон"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Пропущ. дзвінки"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"Пропущ. дзвінк: <xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Пропущ. виклик від <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Швидка відповідь"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Повідомлення надіслано на номер <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Власник пристрою дозволив лише екстрені виклики"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Облікові записи для дзвінків"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Дозволено лише екстрені виклики."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Цей додаток не може телефонувати без відповідного дозволу."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Щоб зателефонувати, введіть дійсний номер."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Зараз не можна почати дзвінок."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Щоб здійснювати відеодзвінки, вимкніть режим TTY."</string>
- <string name="no_vm_number" msgid="4164780423805688336">"Відстун. номер голос. пошти"</string>
+ <string name="no_vm_number" msgid="4164780423805688336">"Відстуній номер голосової пошти"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"На SIM-карті немає збереж. номерів голос. пошти."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Додати номер"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Змінити додаток для дзвінків за умовчанням?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Зробити <xliff:g id="NEW_APP">%1$s</xliff:g> додатком для дзвінків за умовчанням замість додатка <xliff:g id="CURRENT_APP">%2$s</xliff:g>?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Зробити <xliff:g id="NEW_APP">%s</xliff:g> додатком для дзвінків за умовчанням?"</string>
</resources>
diff --git a/res/values-ur-rPK/strings.xml b/res/values-ur-rPK/strings.xml
index ef9b16a..ccb3eb0 100644
--- a/res/values-ur-rPK/strings.xml
+++ b/res/values-ur-rPK/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"فون"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"فون کال کا نظم"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"فون"</string>
<string name="unknown" msgid="6878797917991465859">"نامعلوم"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"چھوٹی ہوئی کال"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"کام سے متعلق چھوٹی ہوئی کال"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"چھوٹی ہوئی کالیں"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> چھوٹی ہوئی کالیں"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> کی جانب سے چھوٹی ہوئی کال"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"فوری جواب"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"پیغام <xliff:g id="PHONE_NUMBER">%s</xliff:g> کو بھیج دیا گیا۔"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"آلہ کے مالک نے صرف ہنگامی کالز کی اجازت دی ہے۔"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"کالنگ اکاؤنٹس"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"صرف ہنگامی کالز کی اجازت ہے۔"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"یہ ایپلی کیشن فون کی اجازت کے بغیر باہر جانے والی کالیں نہیں کر سکتی۔"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"کال کرنے کیلئے، ایک درست نمبر درج کریں۔"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"اس وقت کال شامل نہیں کی جا سکتی ہے۔"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"براہ کرم ویڈیو کالز کرنے کیلئے TTY وضع غیر فعال کریں۔"</string>
<string name="no_vm_number" msgid="4164780423805688336">"صوتی میل نمبر درج نہیں ہے"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM کارڈ پر کوئی بھی صوتی میل نمبر اسٹور نہیں ہے۔"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"نمبر شامل کریں"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"ڈیفالٹ ڈائلر ایپ تبدیل کریں"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"<xliff:g id="CURRENT_APP">%2$s</xliff:g> کی بجائے <xliff:g id="NEW_APP">%1$s</xliff:g> کو بطور اپنی ڈیفالٹ ڈائلر ایپ استعمال کریں؟"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> کو بطور اپنی ڈیفالٹ ڈائلر ایپ استعمال کریں؟"</string>
</resources>
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml
index 1cd5da6..c42152f 100644
--- a/res/values-uz-rUZ/strings.xml
+++ b/res/values-uz-rUZ/strings.xml
@@ -16,30 +16,36 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Telefon"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Telefon – qo‘ng‘iroqlarni boshqarish"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Telefon"</string>
<string name="unknown" msgid="6878797917991465859">"Noma’lum"</string>
- <string name="notification_missedCallTitle" msgid="7554385905572364535">"Javob berilmagan qo‘ng‘iroq"</string>
+ <string name="notification_missedCallTitle" msgid="7554385905572364535">"Javobsiz qo‘ng‘iroq"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Javobsiz ishchi qo‘ng‘irog‘i"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Javobsiz qo‘ng‘iroqlar"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ta javobsiz qo‘ng‘iroq"</string>
- <string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>dan javobsiz qo‘ng‘iroq"</string>
- <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Teskari qo‘ng‘iroq"</string>
- <string name="notification_missedCall_message" msgid="3049928912736917988">"Xabar"</string>
+ <string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> qo‘ng‘irog‘i javobsiz qoldirildi"</string>
+ <string name="notification_missedCall_call_back" msgid="2684890353590890187">"Telefon"</string>
+ <string name="notification_missedCall_message" msgid="3049928912736917988">"SMS"</string>
<string name="accessibility_call_muted" msgid="2776111226185342220">"Qo‘ng‘iroq ovozi o‘chirildi."</string>
<string name="accessibility_speakerphone_enabled" msgid="1988512040421036359">"Karnaychalar yoqildi."</string>
<string name="respond_via_sms_canned_response_1" msgid="2461606462788380215">"Hozir gaplasholmayman. Tinchlikmi?"</string>
- <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Hozir qaytarib qo‘ng‘iroq qilaman."</string>
- <string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Keyinroq qo‘ng‘iroq qilaman."</string>
- <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Bandman. Keyin qo‘n-q qilasizmi?"</string>
+ <string name="respond_via_sms_canned_response_2" msgid="4074450431532859214">"Hozir telefon qilaman."</string>
+ <string name="respond_via_sms_canned_response_3" msgid="3496079065723960450">"Keyinroq telefon qilaman."</string>
+ <string name="respond_via_sms_canned_response_4" msgid="1698989243040062190">"Bandman, keyinroq telefon qiling."</string>
<string name="respond_via_sms_setting_title" msgid="3754000371039709383">"Tezkor javoblar"</string>
- <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Tezkor javoblarni tahrirlash"</string>
+ <string name="respond_via_sms_setting_title_2" msgid="6104662227299493906">"Tezkor javoblar"</string>
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Tezkor javob"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Xabar <xliff:g id="PHONE_NUMBER">%s</xliff:g>ga jo‘natildi."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Qurilma egasi faqat favqulodda qo‘ng‘iroqlarga ruxsat bergan"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Qo‘ng‘iroq hisoblari"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Faqat favqulodda qo‘ng‘iroqlarga ruxsat berilgan."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Bu ilova Telefon ruxsatnomasisiz chiquvchi qo‘ng‘iroqlarni amalga oshira olmaydi."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Qo‘ng‘iroq qilish uchun raqamni to‘g‘ri kiriting."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Hozirgi vaqtda qo‘ng‘iroq qo‘shib bo‘lmaydi."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Video qo\'ng\'iroqlarni amalga oshirish uchun TTY rejimini o‘chirib qo‘yish kerak"</string>
- <string name="no_vm_number" msgid="4164780423805688336">"Javobsiz ovozli xabar raqami"</string>
- <string name="no_vm_number_msg" msgid="1300729501030053828">"SIM kartada birorta ham ovozli xabar saqlanmagan."</string>
+ <string name="no_vm_number" msgid="4164780423805688336">"Ovozli pochta raqami ko‘rsatilmagan"</string>
+ <string name="no_vm_number_msg" msgid="1300729501030053828">"SIM kartada birorta ham ovozli pochta raqami yo‘q."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Raqam qo‘shish"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Birlamchi raqam terish ilovasi o‘zgartirilsinmi?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Raqam terish uchun birlamchi ilova sifatida <xliff:g id="CURRENT_APP">%2$s</xliff:g> o‘rniga <xliff:g id="NEW_APP">%1$s</xliff:g> ilovasi tanlansinmi?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"<xliff:g id="NEW_APP">%s</xliff:g> raqam terish uchun birlamchi ilova sifatida tanlansinmi?"</string>
</resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 3b7e1b4..df5f1c4 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Điện thoại"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Quản lý cuộc gọi điện thoại"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Điện thoại"</string>
<string name="unknown" msgid="6878797917991465859">"Không xác định"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Cuộc gọi nhỡ"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Cuộc gọi nhỡ về công việc"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Cuộc gọi nhỡ"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> cuộc gọi nhỡ"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Cuộc gọi nhỡ từ <xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Trả lời nhanh"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Đã gửi tin nhắn tới <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Chỉ chủ sở hữu thiết bị mới được phép thực hiện cuộc gọi khẩn cấp"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Tài khoản gọi"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Chỉ được phép thực hiện cuộc gọi khẩn cấp."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Ứng dụng này không thể thực hiện cuộc gọi đi mà không có quyền của Điện thoại."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Để thực hiện cuộc gọi, hãy nhập một số hợp lệ."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Không thể thêm cuộc gọi tại thời điểm này."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Vui lòng tắt Chế độ TTY để thực hiện cuộc gọi điện video."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Thiếu số thư thoại"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Không có số thư thoại nào được lưu trữ trên thẻ SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Thêm số điện thoại"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Thay đổi ứng dụng Trình quay số mặc định?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Sử dụng <xliff:g id="NEW_APP">%1$s</xliff:g> làm ứng dụng trình quay số mặc định của bạn thay vì <xliff:g id="CURRENT_APP">%2$s</xliff:g>?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Sử dụng <xliff:g id="NEW_APP">%s</xliff:g> làm ứng dụng trình quay số mặc định của bạn?"</string>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 2d06096..eda2169 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"电话"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"通话管理"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"电话"</string>
<string name="unknown" msgid="6878797917991465859">"未知"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"未接电话"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"未接工作来电"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"未接电话"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> 个未接电话"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"来自<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>的未接电话"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"快速回复"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"讯息已发送至 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"设备机主仅允许拨打紧急呼救电话"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"通话帐号"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"只能拨打紧急呼救电话。"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"此应用没有电话权限,无法拨出电话。"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"要拨打电话,请输入有效的电话号码。"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"目前无法添加通话。"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"要进行视频通话,请停用 TTY 模式。"</string>
<string name="no_vm_number" msgid="4164780423805688336">"缺少语音信箱号码"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM卡上未存储语音信箱号码。"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"添加号码"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"要更改默认拨号器应用吗?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"要使用<xliff:g id="NEW_APP">%1$s</xliff:g>(而非<xliff:g id="CURRENT_APP">%2$s</xliff:g>)作为您的默认拨号器应用吗?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"要使用<xliff:g id="NEW_APP">%s</xliff:g>作为您的默认拨号器应用吗?"</string>
</resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index c00d0b4..7310bc0 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"電話"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"手機通話管理"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"電話"</string>
<string name="unknown" msgid="6878797917991465859">"未知"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"未接來電"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"未接工作來電"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"未接來電"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> 未接電話"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>打來的未接來電"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"快速回應"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"訊息已傳送至 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"只有裝置擁有者才可撥打緊急電話"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"通話帳戶"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"只限緊急電話。"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"此應用程式沒有電話權限,無法撥出電話。"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"要撥打電話,請輸入有效的號碼。"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"目前無法新增視像通話。"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"請停用 TTY 模式以進行視像通話。"</string>
<string name="no_vm_number" msgid="4164780423805688336">"未填留言信箱號碼"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM 卡中沒有儲存任何留言信箱號碼。"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"新增電話號碼"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"變更預設撥號器應用程式?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"使用 <xliff:g id="NEW_APP">%1$s</xliff:g> 取代 <xliff:g id="CURRENT_APP">%2$s</xliff:g> 作為預設撥號器應用程式?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"使用 <xliff:g id="NEW_APP">%s</xliff:g> 為預設撥號器應用程式?"</string>
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 231d938..137f641 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"電話"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"通話管理"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"電話"</string>
<string name="unknown" msgid="6878797917991465859">"不明"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"未接來電"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"未接公司來電"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"未接來電"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> 通未接來電"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"來自 <xliff:g id="MISSED_CALL_FROM">%s</xliff:g> 的未接來電"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"快速回應"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"訊息已傳送至 <xliff:g id="PHONE_NUMBER">%s</xliff:g>。"</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"裝置擁有者限定只能撥打緊急電話"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"通話帳戶"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"只能撥打緊急電話。"</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"這個應用程式不具備電話應用程式存取權限,無法撥出電話。"</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"如要撥打電話,請輸入有效的號碼。"</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"目前無法新增通話。"</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"如要進行視訊通話,請停用 TTY 模式。"</string>
<string name="no_vm_number" msgid="4164780423805688336">"遺失語音信箱號碼"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"SIM 卡中未儲存語音信箱號碼。"</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"新增號碼"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"要變更預設撥號應用程式嗎?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"要將 <xliff:g id="NEW_APP">%1$s</xliff:g> (而非 <xliff:g id="CURRENT_APP">%2$s</xliff:g>) 設為預設撥號應用程式嗎?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"要將 <xliff:g id="NEW_APP">%s</xliff:g> 設為預設撥號應用程式嗎?"</string>
</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index acb00f3..5e1cad7 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -16,9 +16,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="telecommAppLabel" product="default" msgid="3477737022166975496">"Ifoni"</string>
+ <string name="telecommAppLabel" product="default" msgid="9166784827254469057">"Ukuphathwa kwekholi yefoni"</string>
+ <string name="userCallActivityLabel" product="default" msgid="5415173590855187131">"Ifoni"</string>
<string name="unknown" msgid="6878797917991465859">"Akwaziwa"</string>
<string name="notification_missedCallTitle" msgid="7554385905572364535">"Ikholi ekulahlekele"</string>
+ <string name="notification_missedWorkCallTitle" msgid="6242489980390803090">"Ugeje ikholi yomsebenzi"</string>
<string name="notification_missedCallsTitle" msgid="1361677948941502522">"Amakholi akuphuthele"</string>
<string name="notification_missedCallsMsg" msgid="4575787816055205600">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> amakholi akulahlekele"</string>
<string name="notification_missedCallTicker" msgid="504686252427747209">"Uphuthelwe ikholi ephuma ku-<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>"</string>
@@ -35,11 +37,15 @@
<string name="respond_via_sms_setting_summary" msgid="9150281183930613065"></string>
<string name="respond_via_sms_edittext_dialog_title" msgid="20379890418289778">"Izimpendulo ezisheshayo"</string>
<string name="respond_via_sms_confirmation_format" msgid="7229149977515784269">"Umlayezo othunyelwe ku <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
- <string name="outgoing_call_not_allowed" msgid="1435394568102165287">"Amakholi wesimo esiphuthumayo kuphela avunyelwe ngumnikazi wedivayisi"</string>
+ <string name="enable_account_preference_title" msgid="2021848090086481720">"Ama-akhawunti wokushaya"</string>
+ <string name="outgoing_call_not_allowed_user_restriction" msgid="6872406278300131364">"Amakholi aphuthumayo kuphela avunyelwe."</string>
+ <string name="outgoing_call_not_allowed_no_permission" msgid="1996571596464271228">"Lolu hlelo lokusebenza alikwazi ukwenza amakhli aphumayo ngaphandle kwemvume yefoni."</string>
<string name="outgoing_call_error_no_phone_number_supplied" msgid="1940125199802007505">"Ukuze wenze ikholi, faka inombolo evumelekile."</string>
<string name="duplicate_video_call_not_allowed" msgid="3749211605014548386">"Ikholi ayikwazi ukungezwa ngalesi sikhathi."</string>
- <string name="video_call_not_allowed_if_tty_enabled" msgid="7593649283571253283">"Sicela ukhubaze imodi ye-TTY ukuze wenze amakholi wevidiyo."</string>
<string name="no_vm_number" msgid="4164780423805688336">"Inombolo engekho yomyalezo wezwi"</string>
<string name="no_vm_number_msg" msgid="1300729501030053828">"Ayikho inombolo yomlayezo wezwi egcinwe ekhadini le-SIM."</string>
<string name="add_vm_number_str" msgid="4676479471644687453">"Engeza inombolo"</string>
+ <string name="change_default_dialer_dialog_title" msgid="4430590714918044425">"Guqula uhlelo lwakho lokusebenza oluzenzakalelayo lokudayela?"</string>
+ <string name="change_default_dialer_with_previous_app_set_text" msgid="3213396537499337949">"Sebenzisa i-<xliff:g id="NEW_APP">%1$s</xliff:g> esikhundleni se-<xliff:g id="CURRENT_APP">%2$s</xliff:g> njengohlelo lwakho lokusebenza oluzenzakalelayo lokokudayela?"</string>
+ <string name="change_default_dialer_no_previous_app_set_text" msgid="7608426684114545221">"Sebenzisa i-<xliff:g id="NEW_APP">%s</xliff:g> njengohlelo lwakho lokusebenza oluzenzakalelayo lokokudayela?"</string>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ac8bd49..16b1752 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -30,6 +30,8 @@
<!-- Notification strings -->
<!-- Missed call notification label, used when there's exactly one missed call -->
<string name="notification_missedCallTitle">Missed call</string>
+ <!-- Missed work call notification label, used when there's exactly one missed call [CHAR LIMIT=NONE] -->
+ <string name="notification_missedWorkCallTitle">Missed work call</string>
<!-- Missed call notification label, used when there are two or more missed calls -->
<string name="notification_missedCallsTitle">Missed calls</string>
<!-- Missed call notification message used when there are multiple missed calls -->
@@ -85,7 +87,7 @@
<!-- Message indicating that the user is not allowed to make non-emergency outgoing phone calls
due to a user restriction -->
- <string name="outgoing_call_not_allowed_user_restriction">Only emergency calls are allowed by the device owner.</string>
+ <string name="outgoing_call_not_allowed_user_restriction">Only emergency calls are allowed.</string>
<!-- Message indicating that the user is not allowed to make non-emergency outgoing phone calls
due to the lack of the CALL_PHONE permission -->
diff --git a/scripts/telecom_testing.sh b/scripts/telecom_testing.sh
new file mode 100644
index 0000000..fa48040
--- /dev/null
+++ b/scripts/telecom_testing.sh
@@ -0,0 +1,92 @@
+lite_test_telecom() {
+ usage="
+ Usage: lite_test_telecom [-c CLASSNAME] [-d] [-a | -i] [-e], where
+
+ -c CLASSNAME Run tests only for the specified class/method. CLASSNAME
+ should be of the form SomeClassTest or SomeClassTest#testMethod.
+ -d Waits for a debugger to attach before starting to run tests.
+ -i Rebuild and reinstall the test apk before running tests (mmm).
+ -a Rebuild all dependencies and reinstall the test apk before/
+ running tests (mmma).
+ -e Run code coverage. Coverage will be output into the coverage/
+ directory in the repo root.
+ -h This help message.
+ "
+
+ OPTIND=1
+ class=
+ install=false
+ installwdep=false
+ debug=false
+ coverage=false
+
+ while getopts "c:hadie" opt; do
+ case "$opt" in
+ h)
+ echo "$usage"
+ return 0;;
+ \?)
+ echo "$usage"
+ return 0;;
+ c)
+ class=$OPTARG;;
+ d)
+ debug=true;;
+ i)
+ install=true;;
+ a)
+ install=true
+ installwdep=true;;
+ e)
+ coverage=true;;
+ esac
+ done
+
+ T=$(gettop)
+
+ if [ $install = true ] ; then
+ olddir=$(pwd)
+ cd $T
+ if [ $coverage = true ] ; then
+ emma_opt="EMMA_INSTRUMENT_STATIC=true"
+ else
+ emma_opt="EMMA_INSTRUMENT_STATIC=false"
+ fi
+ # Build and exit script early if build fails
+ if [ $installwdep = true ] ; then
+ ANDROID_COMPILE_WITH_JACK=false mmma -j40 "packages/services/Telecomm/tests" ${emma_opt}
+ else
+ ANDROID_COMPILE_WITH_JACK=false mmm "packages/services/Telecomm/tests" ${emma_opt}
+ fi
+ if [ $? -ne 0 ] ; then
+ echo "Make failed! try using -a instead of -i if building with coverage"
+ return
+ fi
+
+ adb install -r -t "out/target/product/$TARGET_PRODUCT/data/app/TelecomUnitTests/TelecomUnitTests.apk"
+ if [ $? -ne 0 ] ; then
+ cd "$olddir"
+ return $?
+ fi
+ cd "$olddir"
+ fi
+
+ e_options=""
+ if [ -n "$class" ] ; then
+ e_options="${e_options} -e class com.android.server.telecom.tests.${class}"
+ fi
+ if [ $debug = true ] ; then
+ e_options="${e_options} -e debug 'true'"
+ fi
+ if [ $coverage = true ] ; then
+ e_options="${e_options} -e coverage 'true'"
+ fi
+ adb shell am instrument ${e_options} -w com.android.server.telecom.tests/android.test.InstrumentationTestRunner
+
+ if [ $coverage = true ] ; then
+ adb root
+ adb wait-for-device
+ adb pull /data/user/0/com.android.server.telecom.tests/files/coverage.ec /tmp/
+ java -cp external/emma/lib/emma.jar emma report -r html -sp packages/services/Telecomm/src -in out/target/common/obj/APPS/TelecomUnitTests_intermediates/coverage.em -in /tmp/coverage.ec
+ fi
+}
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
new file mode 100644
index 0000000..0ba8592
--- /dev/null
+++ b/src/com/android/server/telecom/Analytics.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcelable;
+import android.telecom.ParcelableCallAnalytics;
+import android.telecom.DisconnectCause;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class that collects and stores data on how calls are being made, in order to
+ * aggregate these into useful statistics.
+ */
+public class Analytics {
+ public static class CallInfo {
+ void setCallStartTime(long startTime) {
+ }
+
+ void setCallEndTime(long endTime) {
+ }
+
+ void setCallIsAdditional(boolean isAdditional) {
+ }
+
+ void setCallIsInterrupted(boolean isInterrupted) {
+ }
+
+ void setCallDisconnectCause(DisconnectCause disconnectCause) {
+ }
+
+ void addCallTechnology(int callTechnology) {
+ }
+
+ void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
+ }
+
+ void setCallConnectionService(String connectionServiceName) {
+ }
+ }
+
+ /**
+ * A class that holds data associated with a call.
+ */
+ @VisibleForTesting
+ public static class CallInfoImpl extends CallInfo {
+ public String callId;
+ 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.
+ 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.
+ public boolean isInterrupted = false; // true if the call was interrupted by an incoming
+ // 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
+ // CallsManager#createCallForExistingConnection, for example, by ImsConference.
+ public boolean createdFromExistingConnection = false;
+
+ public DisconnectCause callTerminationReason;
+ public String connectionService;
+ public boolean isEmergency = false;
+
+ CallInfoImpl(String callId, int callDirection) {
+ this.callId = callId;
+ startTime = 0;
+ endTime = 0;
+ this.callDirection = callDirection;
+ callTechnologies = 0;
+ connectionService = "";
+ }
+
+ CallInfoImpl(CallInfoImpl other) {
+ this.callId = other.callId;
+ this.startTime = other.startTime;
+ this.endTime = other.endTime;
+ this.callDirection = other.callDirection;
+ this.isAdditionalCall = other.isAdditionalCall;
+ this.isInterrupted = other.isInterrupted;
+ this.callTechnologies = other.callTechnologies;
+ this.createdFromExistingConnection = other.createdFromExistingConnection;
+ this.connectionService = other.connectionService;
+ this.isEmergency = other.isEmergency;
+
+ if (other.callTerminationReason != null) {
+ this.callTerminationReason = new DisconnectCause(
+ other.callTerminationReason.getCode(),
+ other.callTerminationReason.getLabel(),
+ other.callTerminationReason.getDescription(),
+ other.callTerminationReason.getReason(),
+ other.callTerminationReason.getTone());
+ } else {
+ this.callTerminationReason = null;
+ }
+ }
+
+ @Override
+ public void setCallStartTime(long startTime) {
+ Log.d(TAG, "setting startTime for call " + callId + " to " + startTime);
+ this.startTime = startTime;
+ }
+
+ @Override
+ public void setCallEndTime(long endTime) {
+ Log.d(TAG, "setting endTime for call " + callId + " to " + endTime);
+ this.endTime = endTime;
+ }
+
+ @Override
+ public void setCallIsAdditional(boolean isAdditional) {
+ Log.d(TAG, "setting isAdditional for call " + callId + " to " + isAdditional);
+ this.isAdditionalCall = isAdditional;
+ }
+
+ @Override
+ public void setCallIsInterrupted(boolean isInterrupted) {
+ Log.d(TAG, "setting isInterrupted for call " + callId + " to " + isInterrupted);
+ this.isInterrupted = isInterrupted;
+ }
+
+ @Override
+ public void addCallTechnology(int callTechnology) {
+ Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology);
+ this.callTechnologies |= callTechnology;
+ }
+
+ @Override
+ public void setCallDisconnectCause(DisconnectCause disconnectCause) {
+ Log.d(TAG, "setting disconnectCause for call " + callId + " to " + disconnectCause);
+ this.callTerminationReason = disconnectCause;
+ }
+
+ @Override
+ public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
+ Log.d(TAG, "setting createdFromExistingConnection for call " + callId + " to "
+ + createdFromExistingConnection);
+ this.createdFromExistingConnection = createdFromExistingConnection;
+ }
+
+ @Override
+ public void setCallConnectionService(String connectionServiceName) {
+ Log.d(TAG, "setting connection service for call " + callId + ": "
+ + connectionServiceName);
+ this.connectionService = connectionServiceName;
+ }
+
+ @Override
+ public String toString() {
+ return "{\n"
+ + " startTime: " + startTime + '\n'
+ + " endTime: " + endTime + '\n'
+ + " direction: " + getCallDirectionString() + '\n'
+ + " isAdditionalCall: " + isAdditionalCall + '\n'
+ + " isInterrupted: " + isInterrupted + '\n'
+ + " callTechnologies: " + getCallTechnologiesAsString() + '\n'
+ + " callTerminationReason: " + getCallDisconnectReasonString() + '\n'
+ + " connectionService: " + connectionService + '\n'
+ + "}\n";
+ }
+
+ public ParcelableCallAnalytics toParcelableAnalytics() {
+ // Rounds up to the nearest second.
+ long callDuration = endTime == 0 ? 0 : endTime - startTime;
+ callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ?
+ 0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND);
+ return new ParcelableCallAnalytics(
+ // rounds down to nearest 5 minute mark
+ startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES,
+ callDuration,
+ callDirection,
+ isAdditionalCall,
+ isInterrupted,
+ callTechnologies,
+ callTerminationReason == null ?
+ ParcelableCallAnalytics.STILL_CONNECTED :
+ callTerminationReason.getCode(),
+ isEmergency,
+ connectionService,
+ createdFromExistingConnection);
+ }
+
+ private String getCallDirectionString() {
+ switch (callDirection) {
+ case UNKNOWN_DIRECTION:
+ return "UNKNOWN";
+ case INCOMING_DIRECTION:
+ return "INCOMING";
+ case OUTGOING_DIRECTION:
+ return "OUTGOING";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private String getCallTechnologiesAsString() {
+ StringBuilder s = new StringBuilder();
+ s.append('[');
+ if ((callTechnologies & CDMA_PHONE) != 0) s.append("CDMA ");
+ if ((callTechnologies & GSM_PHONE) != 0) s.append("GSM ");
+ if ((callTechnologies & SIP_PHONE) != 0) s.append("SIP ");
+ if ((callTechnologies & IMS_PHONE) != 0) s.append("IMS ");
+ if ((callTechnologies & THIRD_PARTY_PHONE) != 0) s.append("THIRD_PARTY ");
+ s.append(']');
+ return s.toString();
+ }
+
+ private String getCallDisconnectReasonString() {
+ if (callTerminationReason != null) {
+ return callTerminationReason.toString();
+ } else {
+ return "NOT SET";
+ }
+ }
+ }
+ public static final String TAG = "TelecomAnalytics";
+
+ // Constants for call direction
+ public static final int UNKNOWN_DIRECTION = ParcelableCallAnalytics.CALLTYPE_UNKNOWN;
+ public static final int INCOMING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_INCOMING;
+ public static final int OUTGOING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_OUTGOING;
+
+ // Constants for call technology
+ public static final int CDMA_PHONE = ParcelableCallAnalytics.CDMA_PHONE;
+ public static final int GSM_PHONE = ParcelableCallAnalytics.GSM_PHONE;
+ public static final int IMS_PHONE = ParcelableCallAnalytics.IMS_PHONE;
+ public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE;
+ public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE;
+
+ public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND;
+
+ private static final Object sLock = new Object(); // Coarse lock for all of analytics
+ private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>();
+
+ public static CallInfo initiateCallAnalytics(String callId, int direction) {
+ Log.d(TAG, "Starting analytics for call " + callId);
+ CallInfoImpl callInfo = new CallInfoImpl(callId, direction);
+ synchronized (sLock) {
+ sCallIdToInfo.put(callId, callInfo);
+ }
+ return callInfo;
+ }
+
+ public static ParcelableCallAnalytics[] dumpToParcelableAnalytics() {
+ ParcelableCallAnalytics[] result;
+ synchronized (sLock) {
+ result = new ParcelableCallAnalytics[sCallIdToInfo.size()];
+ int idx = 0;
+ for (CallInfoImpl entry : sCallIdToInfo.values()) {
+ result[idx] = entry.toParcelableAnalytics();
+ idx++;
+ }
+ sCallIdToInfo.clear();
+ }
+ return result;
+ }
+
+ public static void dump(IndentingPrintWriter writer) {
+ synchronized (sLock) {
+ for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
+ writer.printf("Call %s: ", entry.getKey());
+ writer.println(entry.getValue().toString());
+ }
+ }
+ }
+
+ public static void reset() {
+ synchronized (sLock) {
+ sCallIdToInfo.clear();
+ }
+ }
+
+ /**
+ * Returns a deep copy of callIdToInfo that's safe to read/write without synchronization
+ */
+ public static Map<String, CallInfoImpl> cloneData() {
+ synchronized (sLock) {
+ Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size());
+ for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
+ result.put(entry.getKey(), new CallInfoImpl(entry.getValue()));
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/AsyncBlockCheckTask.java b/src/com/android/server/telecom/AsyncBlockCheckTask.java
new file mode 100644
index 0000000..282a4eb
--- /dev/null
+++ b/src/com/android/server/telecom/AsyncBlockCheckTask.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.AsyncTask;
+import android.os.Handler;
+
+import com.android.internal.telephony.BlockChecker;
+
+/**
+ * An {@link AsyncTask} that checks if a call needs to be blocked.
+ * <p> An {@link AsyncTask} is used to perform the block check to avoid blocking the main thread.
+ * The block check itself is performed in the {@link AsyncTask#doInBackground(Object[])}. However
+ * a {@link Handler} passed by the caller is used to perform additional possibly intensive
+ * operations such as call screening.
+ */
+class AsyncBlockCheckTask extends AsyncTask<String, Void, Boolean> {
+ private final Context mContext;
+ private final Call mIncomingCall;
+ private final CallScreening mCallScreening;
+ private final boolean mShouldSendToVoicemail;
+ private final Handler mHandler = new Handler();
+ private final Object mCallScreeningListenerLock = new Object();
+
+ private Session mLogSubsession;
+ private Runnable mBlockCheckTimeoutRunnable = new Runnable("ABCT.oPE") {
+ @Override
+ public void loggedRun() {
+ synchronized (mCallScreeningListenerLock) {
+ if (mCallScreeningListener != null) {
+ timeoutBlockCheck();
+ mCallScreeningListener = null;
+ }
+ }
+ }
+ };
+ private CallScreening.Listener mCallScreeningListener;
+
+ AsyncBlockCheckTask(Context context, Call incomingCall, CallScreening callScreening,
+ CallScreening.Listener callScreeningListener,
+ boolean shouldSendToVoicemail) {
+ mContext = context;
+ mIncomingCall = incomingCall;
+ mCallScreening = callScreening;
+ mCallScreeningListener = callScreeningListener;
+ mShouldSendToVoicemail = shouldSendToVoicemail;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mLogSubsession = Log.createSubsession();
+ mHandler.postDelayed(mBlockCheckTimeoutRunnable.prepare(),
+ Timeouts.getBlockCheckTimeoutMillis(mContext.getContentResolver()));
+ }
+
+ private void timeoutBlockCheck() {
+ Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_TIMED_OUT);
+ mCallScreeningListener.onCallScreeningCompleted(
+ mIncomingCall,
+ true /*shouldAllowCall*/,
+ false /*shouldReject*/,
+ false /*shouldAddToCallLog*/,
+ false /*shouldShowNotification*/);
+ }
+
+ @Override
+ protected Boolean doInBackground(String... params) {
+ try {
+ Log.continueSession(mLogSubsession, "ABCT.DIB");
+ Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_INITIATED);
+ return BlockChecker.isBlocked(mContext, params[0]);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isBlocked) {
+ synchronized (mCallScreeningListenerLock) {
+ mHandler.removeCallbacks(null);
+ mBlockCheckTimeoutRunnable.cancel();
+ if (mCallScreeningListener != null) {
+ processIsBlockedLocked(isBlocked);
+ mCallScreeningListener = null;
+ }
+ }
+ }
+
+ private void processIsBlockedLocked(boolean isBlocked) {
+ Log.event(mIncomingCall, Log.Events.BLOCK_CHECK_FINISHED);
+ if (isBlocked) {
+ mCallScreeningListener.onCallScreeningCompleted(
+ mIncomingCall,
+ false /*shouldAllowCall*/,
+ true /*shouldReject*/,
+ false /*shouldAddToCallLog*/,
+ false /*shouldShowNotification*/);
+ } else if (mShouldSendToVoicemail) {
+ mCallScreeningListener.onCallScreeningCompleted(
+ mIncomingCall,
+ false /*shouldAllowCall*/,
+ true /*shouldReject*/,
+ true /*shouldAddToCallLog*/,
+ true /*shouldShowNotification*/);
+ } else {
+ mCallScreening.screenCall();
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 3030fea..f597cf1 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -16,23 +16,20 @@
package com.android.server.telecom;
-import android.content.Context;
-import android.media.AudioManager;
import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
-import android.provider.Settings;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
/**
* Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
* used from the main thread.
*/
-class AsyncRingtonePlayer {
+@VisibleForTesting
+public class AsyncRingtonePlayer {
// Message codes used with the ringtone thread.
private static final int EVENT_PLAY = 1;
private static final int EVENT_STOP = 2;
@@ -47,23 +44,18 @@
/** The current ringtone. Only used by the ringtone thread. */
private Ringtone mRingtone;
- /**
- * The context.
- */
- private final Context mContext;
-
- AsyncRingtonePlayer(Context context) {
- mContext = context;
- }
-
/** Plays the ringtone. */
- void play(Uri ringtone) {
+ public void play(Ringtone ringtone) {
Log.d(this, "Posting play.");
+ if (ringtone == null) {
+ Log.i(this, "null (silence -- not playing anything)");
+ return;
+ }
postMessage(EVENT_PLAY, true /* shouldCreateHandler */, ringtone);
}
/** Stops playing the ringtone. */
- void stop() {
+ public void stop() {
Log.d(this, "Posting stop.");
postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
}
@@ -75,7 +67,7 @@
* @param messageCode The message to post.
* @param shouldCreateHandler True when a handler should be created to handle this message.
*/
- private void postMessage(int messageCode, boolean shouldCreateHandler, Uri ringtone) {
+ private void postMessage(int messageCode, boolean shouldCreateHandler, Ringtone ringtone) {
synchronized(this) {
if (mHandler == null && shouldCreateHandler) {
mHandler = getNewHandler();
@@ -103,7 +95,7 @@
public void handleMessage(Message msg) {
switch(msg.what) {
case EVENT_PLAY:
- handlePlay((Uri) msg.obj);
+ handlePlay((Ringtone) msg.obj);
break;
case EVENT_REPEAT:
handleRepeat();
@@ -119,7 +111,7 @@
/**
* Starts the actual playback of the ringtone. Executes on ringtone-thread.
*/
- private void handlePlay(Uri ringtoneUri) {
+ private void handlePlay(Ringtone ringtone) {
// don't bother with any of this if there is an EVENT_STOP waiting.
if (mHandler.hasMessages(EVENT_STOP)) {
return;
@@ -129,13 +121,7 @@
Log.i(this, "Play ringtone.");
if (mRingtone == null) {
- mRingtone = getRingtone(ringtoneUri);
-
- // Cancel everything if there is no ringtone.
- if (mRingtone == null) {
- handleStop();
- return;
- }
+ mRingtone = ringtone;
}
handleRepeat();
@@ -189,16 +175,4 @@
}
}
}
-
- private Ringtone getRingtone(Uri ringtoneUri) {
- if (ringtoneUri == null) {
- ringtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
- }
-
- Ringtone ringtone = RingtoneManager.getRingtone(mContext, ringtoneUri);
- if (ringtone != null) {
- ringtone.setStreamType(AudioManager.STREAM_RING);
- }
- return ringtone;
- }
}
diff --git a/src/com/android/server/telecom/BluetoothHeadsetProxy.java b/src/com/android/server/telecom/BluetoothHeadsetProxy.java
new file mode 100644
index 0000000..48c60a1
--- /dev/null
+++ b/src/com/android/server/telecom/BluetoothHeadsetProxy.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.bluetooth.BluetoothHeadset;
+
+/**
+ * A proxy class that facilitates testing of the BluetoothPhoneServiceImpl class.
+ *
+ * This is necessary due to the "final" attribute of the BluetoothHeadset class. In order to
+ * test the correct functioning of the BluetoothPhoneServiceImpl class, the final class must be put
+ * into a container that can be mocked correctly.
+ */
+public class BluetoothHeadsetProxy {
+
+ private BluetoothHeadset mBluetoothHeadset;
+
+ public BluetoothHeadsetProxy(BluetoothHeadset headset) {
+ mBluetoothHeadset = headset;
+ }
+
+ public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
+ String number, int type) {
+
+ mBluetoothHeadset.clccResponse(index, direction, status, mode, mpty, number, type);
+ }
+
+ public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
+ int type) {
+
+ mBluetoothHeadset.phoneStateChanged(numActive, numHeld, callState, number, type);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/BluetoothManager.java b/src/com/android/server/telecom/BluetoothManager.java
index aec0d3e..8dd5a7b 100644
--- a/src/com/android/server/telecom/BluetoothManager.java
+++ b/src/com/android/server/telecom/BluetoothManager.java
@@ -24,8 +24,11 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
import android.os.SystemClock;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.List;
@@ -35,21 +38,34 @@
* overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
*/
public class BluetoothManager {
+ public interface BluetoothStateListener {
+ void onBluetoothStateChange(BluetoothManager bluetoothManager);
+ }
private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
- mBluetoothHeadset = (BluetoothHeadset) proxy;
- Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
- updateBluetoothState();
+ Log.startSession("BMSL.oSC");
+ try {
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
+ updateBluetoothState();
+ } finally {
+ Log.endSession();
+ }
}
@Override
public void onServiceDisconnected(int profile) {
- mBluetoothHeadset = null;
- Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset);
- updateBluetoothState();
+ Log.startSession("BMSL.oSD");
+ try {
+ mBluetoothHeadset = null;
+ Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset);
+ updateBluetoothState();
+ } finally {
+ Log.endSession();
+ }
}
};
@@ -59,36 +75,55 @@
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
+ Log.startSession("BM.oR");
+ try {
+ String action = intent.getAction();
- if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
- int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
- BluetoothHeadset.STATE_DISCONNECTED);
- Log.d(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
- Log.d(this, "==> new state: %s ", bluetoothHeadsetState);
- updateBluetoothState();
- } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
- int bluetoothHeadsetAudioState =
- intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
- BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
- Log.d(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
- Log.d(this, "==> new state: %s", bluetoothHeadsetAudioState);
- updateBluetoothState();
+ if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+ int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_DISCONNECTED);
+ Log.d(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
+ Log.d(this, "==> new state: %s ", bluetoothHeadsetState);
+ updateBluetoothState();
+ } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+ int bluetoothHeadsetAudioState =
+ intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+ Log.d(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
+ Log.d(this, "==> new state: %s", bluetoothHeadsetAudioState);
+ updateBluetoothState();
+ }
+ } finally {
+ Log.endSession();
}
}
};
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
private final BluetoothAdapter mBluetoothAdapter;
- private final CallAudioManager mCallAudioManager;
+ private BluetoothStateListener mBluetoothStateListener;
private BluetoothHeadset mBluetoothHeadset;
private boolean mBluetoothConnectionPending = false;
private long mBluetoothConnectionRequestTime;
+ private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA") {
+ @Override
+ public void loggedRun() {
+ if (!isBluetoothAudioConnected()) {
+ Log.v(this, "Bluetooth audio inexplicably disconnected within 5 seconds of " +
+ "connection. Updating UI.");
+ }
+ mBluetoothConnectionPending = false;
+ updateBluetoothState();
+ }
+ };
+ private final Context mContext;
- public BluetoothManager(Context context, CallAudioManager callAudioManager) {
+ public BluetoothManager(Context context) {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- mCallAudioManager = callAudioManager;
+ mContext = context;
if (mBluetoothAdapter != null) {
mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -102,6 +137,10 @@
context.registerReceiver(mReceiver, intentFilter);
}
+ public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) {
+ mBluetoothStateListener = bluetoothStateListener;
+ }
+
//
// Bluetooth helper methods.
//
@@ -121,7 +160,8 @@
* available to the user (i.e. if the device is BT-capable
* and a headset is connected.)
*/
- boolean isBluetoothAvailable() {
+ @VisibleForTesting
+ public boolean isBluetoothAvailable() {
Log.v(this, "isBluetoothAvailable()...");
// There's no need to ask the Bluetooth system service if BT is enabled:
@@ -191,7 +231,8 @@
* that the BT audio connection is currently being set
* up, and will be connected soon.)
*/
- /* package */ boolean isBluetoothAudioConnectedOrPending() {
+ @VisibleForTesting
+ public boolean isBluetoothAudioConnectedOrPending() {
if (isBluetoothAudioConnected()) {
Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
return true;
@@ -203,16 +244,9 @@
if (mBluetoothConnectionPending) {
long timeSinceRequest =
SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
- if (timeSinceRequest < 5000 /* 5 seconds */) {
- Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
- + timeSinceRequest + " msec ago)");
- return true;
- } else {
- Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
- + timeSinceRequest + " msec ago)");
- mBluetoothConnectionPending = false;
- return false;
- }
+ Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+ + timeSinceRequest + " msec ago)");
+ return true;
}
Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
@@ -223,10 +257,11 @@
* Notified audio manager of a change to the bluetooth state.
*/
void updateBluetoothState() {
- mCallAudioManager.onBluetoothStateChange(this);
+ mBluetoothStateListener.onBluetoothStateChange(this);
}
- void connectBluetoothAudio() {
+ @VisibleForTesting
+ public void connectBluetoothAudio() {
Log.v(this, "connectBluetoothAudio()...");
if (mBluetoothHeadset != null) {
mBluetoothHeadset.connectAudio();
@@ -239,13 +274,22 @@
// instantly. (See isBluetoothAudioConnectedOrPending() above.)
mBluetoothConnectionPending = true;
mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
+ mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
+ mBluetoothConnectionTimeout.cancel();
+ // If the mBluetoothConnectionTimeout runnable has run, the session had been cleared...
+ // Create a new Session before putting it back in the queue to possibly run again.
+ mHandler.postDelayed(mBluetoothConnectionTimeout.prepare(),
+ Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver()));
}
- void disconnectBluetoothAudio() {
+ @VisibleForTesting
+ public void disconnectBluetoothAudio() {
Log.v(this, "disconnectBluetoothAudio()...");
if (mBluetoothHeadset != null) {
mBluetoothHeadset.disconnectAudio();
}
+ mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
+ mBluetoothConnectionTimeout.cancel();
mBluetoothConnectionPending = false;
}
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
index dab4545..6680eb6 100644
--- a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -34,6 +34,7 @@
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.CallsManager.CallsManagerListener;
import java.util.Collection;
@@ -45,7 +46,13 @@
* Bluetooth headset manager for Telecom. This class shares the call state with the bluetooth device
* and accepts call-related commands to perform on behalf of the BT device.
*/
-public final class BluetoothPhoneServiceImpl {
+public class BluetoothPhoneServiceImpl {
+
+ public interface BluetoothPhoneServiceImplFactory {
+ BluetoothPhoneServiceImpl makeBluetoothPhoneServiceImpl(Context context,
+ TelecomSystem.SyncRoot lock, CallsManager callsManager,
+ PhoneAccountRegistrar phoneAccountRegistrar);
+ }
private static final String TAG = "BluetoothPhoneService";
@@ -79,12 +86,13 @@
* Binder implementation of IBluetoothHeadsetPhone. Implements the command interface that the
* bluetooth headset code uses to control call.
*/
- private final IBluetoothHeadsetPhone.Stub mBinder = new IBluetoothHeadsetPhone.Stub() {
+ @VisibleForTesting
+ public final IBluetoothHeadsetPhone.Stub mBinder = new IBluetoothHeadsetPhone.Stub() {
@Override
public boolean answerCall() throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.aC");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "BT - answering call");
@@ -96,6 +104,7 @@
return false;
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@@ -105,7 +114,7 @@
public boolean hangupCall() throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.hC");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "BT - hanging up call");
@@ -117,6 +126,7 @@
return false;
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -125,7 +135,7 @@
public boolean sendDtmf(int dtmf) throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.sD");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "BT - sendDtmf %c", Log.DEBUG ? dtmf : '.');
@@ -140,6 +150,7 @@
return false;
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -148,7 +159,7 @@
public String getNetworkOperator() throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.gNO");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "getNetworkOperator");
@@ -162,6 +173,7 @@
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -170,7 +182,7 @@
public String getSubscriberNumber() throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.gSN");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "getSubscriberNumber");
@@ -189,6 +201,7 @@
return address;
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -197,7 +210,7 @@
public boolean listCurrentCalls() throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.lCC");
long token = Binder.clearCallingIdentity();
try {
// only log if it is after we recently updated the headset state or else it can
@@ -213,6 +226,7 @@
return true;
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -221,7 +235,7 @@
public boolean queryPhoneState() throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.qPS");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "queryPhoneState");
@@ -229,6 +243,7 @@
return true;
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -237,13 +252,14 @@
public boolean processChld(int chld) throws RemoteException {
synchronized (mLock) {
enforceModifyPermission();
-
+ Log.startSession("BPSI.pC");
long token = Binder.clearCallingIdentity();
try {
Log.i(TAG, "processChld %d", chld);
return BluetoothPhoneServiceImpl.this.processChld(chld);
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -271,7 +287,8 @@
* Listens to call changes from the CallsManager and calls into methods to update the bluetooth
* headset with the new states.
*/
- private CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() {
+ @VisibleForTesting
+ public CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() {
@Override
public void onCallAdded(Call call) {
updateHeadsetWithCallState(false /* force */);
@@ -309,12 +326,6 @@
}
@Override
- public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
- // The BluetoothPhoneService does not need to respond to changes in foreground calls,
- // which are always accompanied by call state changes anyway.
- }
-
- @Override
public void onIsConferencedChanged(Call call) {
/*
* Filter certain onIsConferencedChanged callbacks. Unfortunately this needs to be done
@@ -350,27 +361,29 @@
* Listens to connections and disconnections of bluetooth headsets. We need to save the current
* bluetooth headset so that we know where to send call updates.
*/
- private BluetoothProfile.ServiceListener mProfileListener =
+ @VisibleForTesting
+ public BluetoothProfile.ServiceListener mProfileListener =
new BluetoothProfile.ServiceListener() {
- @Override
- public void onServiceConnected(int profile, BluetoothProfile proxy) {
- synchronized (mLock) {
- mBluetoothHeadset = (BluetoothHeadset) proxy;
- }
- }
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ synchronized (mLock) {
+ setBluetoothHeadset(new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
+ }
+ }
- @Override
- public void onServiceDisconnected(int profile) {
- synchronized (mLock) {
- mBluetoothHeadset = null;
- }
- }
- };
+ @Override
+ public void onServiceDisconnected(int profile) {
+ synchronized (mLock) {
+ mBluetoothHeadset = null;
+ }
+ }
+ };
/**
* Receives events for global state changes of the bluetooth adapter.
*/
- private final BroadcastReceiver mBluetoothAdapterReceiver = new BroadcastReceiver() {
+ @VisibleForTesting
+ public final BroadcastReceiver mBluetoothAdapterReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
synchronized (mLock) {
@@ -389,7 +402,7 @@
};
private BluetoothAdapter mBluetoothAdapter;
- private BluetoothHeadset mBluetoothHeadset;
+ private BluetoothHeadsetProxy mBluetoothHeadset;
// A map from Calls to indexes used to identify calls for CLCC (C* List Current Calls).
private Map<Call, Integer> mClccIndexMap = new HashMap<>();
@@ -431,6 +444,11 @@
updateHeadsetWithCallState(false /* force */);
}
+ @VisibleForTesting
+ public void setBluetoothHeadset(BluetoothHeadsetProxy bluetoothHeadset) {
+ mBluetoothHeadset = bluetoothHeadset;
+ }
+
private boolean processChld(int chld) {
Call activeCall = mCallsManager.getActiveCall();
Call ringingCall = mCallsManager.getRingingCall();
@@ -485,7 +503,7 @@
if (!conferenceable.isEmpty()) {
mCallsManager.conference(activeCall, conferenceable.get(0));
return true;
- }
+ }
}
}
}
@@ -544,7 +562,7 @@
boolean shouldReevaluateState =
conferenceCall.can(Connection.CAPABILITY_MERGE_CONFERENCE) ||
(conferenceCall.can(Connection.CAPABILITY_SWAP_CONFERENCE) &&
- !conferenceCall.wasConferencePreviouslyMerged());
+ !conferenceCall.wasConferencePreviouslyMerged());
if (shouldReevaluateState) {
isPartOfConference = false;
@@ -621,7 +639,6 @@
* changed.
*/
private void updateHeadsetWithCallState(boolean force) {
- CallsManager callsManager = mCallsManager;
Call activeCall = mCallsManager.getActiveCall();
Call ringingCall = mCallsManager.getRingingCall();
Call heldCall = mCallsManager.getHeldCall();
@@ -676,11 +693,11 @@
(force ||
(!callsPendingSwitch &&
(numActiveCalls != mNumActiveCalls ||
- numHeldCalls != mNumHeldCalls ||
- bluetoothCallState != mBluetoothCallState ||
- !TextUtils.equals(ringingAddress, mRingingAddress) ||
- ringingAddressType != mRingingAddressType ||
- (heldCall != mOldHeldCall && !ignoreHeldCallChange))))) {
+ numHeldCalls != mNumHeldCalls ||
+ bluetoothCallState != mBluetoothCallState ||
+ !TextUtils.equals(ringingAddress, mRingingAddress) ||
+ ringingAddressType != mRingingAddressType ||
+ (heldCall != mOldHeldCall && !ignoreHeldCallChange))))) {
// If the call is transitioning into the alerting state, send DIALING first.
// Some devices expect to see a DIALING state prior to seeing an ALERTING state
@@ -812,15 +829,15 @@
PhoneAccount account = null;
if (call != null) {
// First try to get the network name of the foreground call.
- account = mPhoneAccountRegistrar.getPhoneAccountCheckCallingUser(
+ account = mPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
call.getTargetPhoneAccount());
}
if (account == null) {
// Second, Try to get the label for the default Phone Account.
- account = mPhoneAccountRegistrar.getPhoneAccountCheckCallingUser(
- mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(
- PhoneAccount.SCHEME_TEL));
+ account = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+ mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ PhoneAccount.SCHEME_TEL));
}
return account;
}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index fbfd2a4..7b342bc 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -29,7 +29,6 @@
import android.telecom.DisconnectCause;
import android.telecom.Connection;
import android.telecom.GatewayInfo;
-import android.telecom.InCallService.VideoCall;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -39,10 +38,12 @@
import android.telecom.VideoProfile;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
+import android.os.UserHandle;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IVideoProvider;
import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
import com.android.internal.telephony.CallerInfoAsyncQuery.OnQueryCompleteListener;
import com.android.internal.telephony.SmsApplication;
import com.android.server.telecom.ContactsAsyncHelper.OnImageLoadCompleteListener;
@@ -68,13 +69,18 @@
public final static String CALL_ID_UNKNOWN = "-1";
public final static long DATA_USAGE_NOT_SET = -1;
+ public static final int CALL_DIRECTION_UNDEFINED = 0;
+ public static final int CALL_DIRECTION_OUTGOING = 1;
+ public static final int CALL_DIRECTION_INCOMING = 2;
+ public static final int CALL_DIRECTION_UNKNOWN = 3;
+
/**
* Listener for events on the call.
*/
interface Listener {
void onSuccessfulOutgoingCall(Call call, int callState);
void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause);
- void onSuccessfulIncomingCall(Call call);
+ void onSuccessfulIncomingCall(Call call, boolean shouldSendToVoicemail);
void onFailedIncomingCall(Call call);
void onSuccessfulUnknownCall(Call call, int callState);
void onFailedUnknownCall(Call call);
@@ -106,7 +112,7 @@
@Override
public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {}
@Override
- public void onSuccessfulIncomingCall(Call call) {}
+ public void onSuccessfulIncomingCall(Call call, boolean shouldSendToVoicemail) {}
@Override
public void onFailedIncomingCall(Call call) {}
@Override
@@ -164,7 +170,10 @@
public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
synchronized (mLock) {
if (cookie != null) {
- ((Call) cookie).setCallerInfo(callerInfo, token);
+ CallSessionCookie callSession = (CallSessionCookie) cookie;
+ Log.continueSession(callSession.mSession, "OQCL.oQC");
+ callSession.mSessionCall.setCallerInfo(callerInfo, token);
+ Log.endSession();
}
}
}
@@ -178,28 +187,34 @@
int token, Drawable photo, Bitmap photoIcon, Object cookie) {
synchronized (mLock) {
if (cookie != null) {
- ((Call) cookie).setPhoto(photo, photoIcon, token);
+ CallSessionCookie callSession = (CallSessionCookie) cookie;
+ Log.continueSession(callSession.mSession, "OCLCL.oILC");
+ callSession.mSessionCall.setPhoto(photo, photoIcon, token);
+ Log.endSession();
}
}
}
};
- private final Runnable mDirectToVoicemailRunnable = new Runnable() {
- @Override
- public void run() {
- synchronized (mLock) {
- processDirectToVoicemail();
- }
+ private class CallSessionCookie {
+ Call mSessionCall;
+ Session mSession;
+
+ public CallSessionCookie(Call call, Session session) {
+ mSessionCall = call;
+ mSession = session;
}
- };
+ }
- /** True if this is an incoming call. */
- private final boolean mIsIncoming;
-
- /** True if this is a currently unknown call that was not previously tracked by CallsManager,
- * and did not originate via the regular incoming/outgoing call code paths.
+ /**
+ * One of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING, or CALL_DIRECTION_UNKNOWN
*/
- private boolean mIsUnknown;
+ private final int mCallDirection;
+
+ /**
+ * The post-dial digits that were dialed after the network portion of the number
+ */
+ private final String mPostDialDigits;
/**
* The time this call was created. Beyond logging and such, may also be used for bookkeeping
@@ -223,6 +238,8 @@
private PhoneAccountHandle mTargetPhoneAccountHandle;
+ private UserHandle mInitiatingUser;
+
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final List<Call> mConferenceableCalls = new ArrayList<>();
@@ -301,6 +318,8 @@
private boolean mIsConference = false;
+ private final boolean mShouldAttachToExistingConnection;
+
private Call mParentCall = null;
private List<Call> mChildCalls = new LinkedList<>();
@@ -323,6 +342,8 @@
private final CallsManager mCallsManager;
private final TelecomSystem.SyncRoot mLock;
private final CallerInfoAsyncQueryFactory mCallerInfoAsyncQueryFactory;
+ private final String mId;
+ private Analytics.CallInfo mAnalytics;
private boolean mWasConferencePreviouslyMerged = false;
@@ -339,6 +360,11 @@
*/
private long mCallDataUsage = DATA_USAGE_NOT_SET;
+ private boolean mIsWorkCall;
+
+ // Set to true once the NewOutgoingCallIntentBroadcast comes back and is processed.
+ private boolean mIsNewOutgoingCallIntentBroadcastDone = false;
+
/**
* Persists the specified parameters and initializes the new instance.
*
@@ -351,9 +377,13 @@
* {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
* @param targetPhoneAccountHandle Account information to use for the call. This account must be
* one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
- * @param isIncoming True if this is an incoming call.
+ * @param callDirection one of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING,
+ * or CALL_DIRECTION_UNKNOWN.
+ * @param shouldAttachToExistingConnection Set to true to attach the call to an existing
+ * connection, regardless of whether it's incoming or outgoing.
*/
public Call(
+ String callId,
Context context,
CallsManager callsManager,
TelecomSystem.SyncRoot lock,
@@ -364,8 +394,10 @@
GatewayInfo gatewayInfo,
PhoneAccountHandle connectionManagerPhoneAccountHandle,
PhoneAccountHandle targetPhoneAccountHandle,
- boolean isIncoming,
+ int callDirection,
+ boolean shouldAttachToExistingConnection,
boolean isConference) {
+ mId = callId;
mState = isConference ? CallState.ACTIVE : CallState.NEW;
mContext = context;
mCallsManager = callsManager;
@@ -374,12 +406,17 @@
mContactsAsyncHelper = contactsAsyncHelper;
mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
setHandle(handle);
+ mPostDialDigits = handle != null
+ ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
mGatewayInfo = gatewayInfo;
setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
setTargetPhoneAccount(targetPhoneAccountHandle);
- mIsIncoming = isIncoming;
+ mCallDirection = callDirection;
mIsConference = isConference;
+ mShouldAttachToExistingConnection = shouldAttachToExistingConnection
+ || callDirection == CALL_DIRECTION_INCOMING;
maybeLoadCannedSmsResponses();
+ mAnalytics = new Analytics.CallInfo();
Log.event(this, Log.Events.CREATED);
}
@@ -396,10 +433,14 @@
* {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
* @param targetPhoneAccountHandle Account information to use for the call. This account must be
* one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
- * @param isIncoming True if this is an incoming call.
+ * @param callDirection one of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING,
+ * or CALL_DIRECTION_UNKNOWN
+ * @param shouldAttachToExistingConnection Set to true to attach the call to an existing
+ * connection, regardless of whether it's incoming or outgoing.
* @param connectTimeMillis The connection time of the call.
*/
Call(
+ String callId,
Context context,
CallsManager callsManager,
TelecomSystem.SyncRoot lock,
@@ -410,15 +451,17 @@
GatewayInfo gatewayInfo,
PhoneAccountHandle connectionManagerPhoneAccountHandle,
PhoneAccountHandle targetPhoneAccountHandle,
- boolean isIncoming,
+ int callDirection,
+ boolean shouldAttachToExistingConnection,
boolean isConference,
long connectTimeMillis) {
- this(context, callsManager, lock, repository, contactsAsyncHelper,
+ this(callId, context, callsManager, lock, repository, contactsAsyncHelper,
callerInfoAsyncQueryFactory, handle, gatewayInfo,
- connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, isIncoming,
- isConference);
+ connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection,
+ shouldAttachToExistingConnection, isConference);
mConnectTimeMillis = connectTimeMillis;
+ mAnalytics.setCallStartTime(connectTimeMillis);
}
public void addListener(Listener listener) {
@@ -431,6 +474,27 @@
}
}
+ public void initAnalytics() {
+ int analyticsDirection;
+ switch (mCallDirection) {
+ case CALL_DIRECTION_OUTGOING:
+ analyticsDirection = Analytics.OUTGOING_DIRECTION;
+ break;
+ case CALL_DIRECTION_INCOMING:
+ analyticsDirection = Analytics.INCOMING_DIRECTION;
+ break;
+ case CALL_DIRECTION_UNKNOWN:
+ case CALL_DIRECTION_UNDEFINED:
+ default:
+ analyticsDirection = Analytics.UNKNOWN_DIRECTION;
+ }
+ mAnalytics = Analytics.initiateCallAnalytics(mId, analyticsDirection);
+ }
+
+ public Analytics.CallInfo getAnalytics() {
+ return mAnalytics;
+ }
+
public void destroy() {
Log.event(this, Log.Events.DESTROYED);
}
@@ -446,7 +510,7 @@
return String.format(Locale.US, "[%s, %s, %s, %s, %s, childs(%d), has_parent(%b), [%s]]",
- System.identityHashCode(this),
+ mId,
CallState.toString(mState),
component,
Log.piiHandle(mHandle),
@@ -484,7 +548,8 @@
return sb.toString();
}
- int getState() {
+ @VisibleForTesting
+ public int getState() {
return mState;
}
@@ -511,6 +576,14 @@
}
/**
+ * Returns the unique ID for this call as it exists in Telecom.
+ * @return The call ID.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
* Sets the call state. Although there exists the notion of appropriate state transitions
* (see {@link CallState}), in practice those expectations break down when cellular systems
* misbehave and they do this very often. The result is that we do not enforce state transitions
@@ -535,6 +608,7 @@
// call from resetting active time when it goes in and out of
// ACTIVE/ON_HOLD
mConnectTimeMillis = System.currentTimeMillis();
+ mAnalytics.setCallStartTime(mConnectTimeMillis);
}
// Video state changes are normally tracked against history when a call is active.
@@ -547,6 +621,7 @@
mDisconnectTimeMillis = 0;
} else if (mState == CallState.DISCONNECTED) {
mDisconnectTimeMillis = System.currentTimeMillis();
+ mAnalytics.setCallEndTime(mDisconnectTimeMillis);
setLocallyDisconnecting(false);
fixParentAfterDisconnect();
}
@@ -609,7 +684,8 @@
return mRingbackRequested;
}
- boolean isConference() {
+ @VisibleForTesting
+ public boolean isConference() {
return mIsConference;
}
@@ -617,6 +693,10 @@
return mHandle;
}
+ public String getPostDialDigits() {
+ return mPostDialDigits;
+ }
+
int getHandlePresentation() {
return mHandlePresentation;
}
@@ -697,6 +777,7 @@
public void setDisconnectCause(DisconnectCause disconnectCause) {
// TODO: Consider combining this method with a setDisconnected() method that is totally
// separate from setState.
+ mAnalytics.setCallDisconnectCause(disconnectCause);
mDisconnectCause = disconnectCause;
}
@@ -704,7 +785,8 @@
return mDisconnectCause;
}
- boolean isEmergencyCall() {
+ @VisibleForTesting
+ public boolean isEmergencyCall() {
return mIsEmergencyCall;
}
@@ -719,7 +801,8 @@
return getHandle();
}
- GatewayInfo getGatewayInfo() {
+ @VisibleForTesting
+ public GatewayInfo getGatewayInfo() {
return mGatewayInfo;
}
@@ -727,11 +810,13 @@
mGatewayInfo = gatewayInfo;
}
- PhoneAccountHandle getConnectionManagerPhoneAccount() {
+ @VisibleForTesting
+ public PhoneAccountHandle getConnectionManagerPhoneAccount() {
return mConnectionManagerPhoneAccountHandle;
}
- void setConnectionManagerPhoneAccount(PhoneAccountHandle accountHandle) {
+ @VisibleForTesting
+ public void setConnectionManagerPhoneAccount(PhoneAccountHandle accountHandle) {
if (!Objects.equals(mConnectionManagerPhoneAccountHandle, accountHandle)) {
mConnectionManagerPhoneAccountHandle = accountHandle;
for (Listener l : mListeners) {
@@ -741,21 +826,52 @@
}
- PhoneAccountHandle getTargetPhoneAccount() {
+ @VisibleForTesting
+ public PhoneAccountHandle getTargetPhoneAccount() {
return mTargetPhoneAccountHandle;
}
- void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
+ @VisibleForTesting
+ public void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
mTargetPhoneAccountHandle = accountHandle;
for (Listener l : mListeners) {
l.onTargetPhoneAccountChanged(this);
}
+ configureIsWorkCall();
}
}
- boolean isIncoming() {
- return mIsIncoming;
+ @VisibleForTesting
+ public boolean isIncoming() {
+ return mCallDirection == CALL_DIRECTION_INCOMING;
+ }
+
+ public boolean isWorkCall() {
+ return mIsWorkCall;
+ }
+
+ private void configureIsWorkCall() {
+ PhoneAccountRegistrar phoneAccountRegistrar = mCallsManager.getPhoneAccountRegistrar();
+ boolean isWorkCall = false;
+ PhoneAccount phoneAccount =
+ phoneAccountRegistrar.getPhoneAccountUnchecked(mTargetPhoneAccountHandle);
+ if (phoneAccount != null) {
+ final UserHandle userHandle;
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
+ userHandle = mInitiatingUser;
+ } else {
+ userHandle = mTargetPhoneAccountHandle.getUserHandle();
+ }
+ if (userHandle != null) {
+ isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
+ }
+ }
+ mIsWorkCall = isWorkCall;
+ }
+
+ boolean shouldAttachToExistingConnection() {
+ return mShouldAttachToExistingConnection;
}
/**
@@ -763,7 +879,8 @@
* period since this call was added to the set pending outgoing calls, see
* mCreationTimeMillis.
*/
- long getAgeMillis() {
+ @VisibleForTesting
+ public long getAgeMillis() {
if (mState == CallState.DISCONNECTED &&
(mDisconnectCause.getCode() == DisconnectCause.REJECTED ||
mDisconnectCause.getCode() == DisconnectCause.MISSED)) {
@@ -816,23 +933,28 @@
}
}
- Call getParentCall() {
+ @VisibleForTesting
+ public Call getParentCall() {
return mParentCall;
}
- List<Call> getChildCalls() {
+ @VisibleForTesting
+ public List<Call> getChildCalls() {
return mChildCalls;
}
- boolean wasConferencePreviouslyMerged() {
+ @VisibleForTesting
+ public boolean wasConferencePreviouslyMerged() {
return mWasConferencePreviouslyMerged;
}
- Call getConferenceLevelActiveCall() {
+ @VisibleForTesting
+ public Call getConferenceLevelActiveCall() {
return mConferenceLevelActiveCall;
}
- ConnectionServiceWrapper getConnectionService() {
+ @VisibleForTesting
+ public ConnectionServiceWrapper getConnectionService() {
return mConnectionService;
}
@@ -845,13 +967,15 @@
return mContext;
}
- void setConnectionService(ConnectionServiceWrapper service) {
+ @VisibleForTesting
+ public void setConnectionService(ConnectionServiceWrapper service) {
Preconditions.checkNotNull(service);
clearConnectionService();
service.incrementAssociatedCallCount();
mConnectionService = service;
+ mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
mConnectionService.addCall(this);
}
@@ -876,20 +1000,20 @@
private void processDirectToVoicemail() {
if (mDirectToVoicemailQueryPending) {
+ boolean shouldSendToVoicemail;
if (mCallerInfo != null && mCallerInfo.shouldSendToVoicemail) {
Log.i(this, "Directing call to voicemail: %s.", this);
// TODO: Once we move State handling from CallsManager to Call, we
// will not need to set STATE_RINGING state prior to calling reject.
- setState(CallState.RINGING, "directing to voicemail");
- reject(false, null);
+ shouldSendToVoicemail = true;
} else {
- // TODO: Make this class (not CallsManager) responsible for changing
- // the call state to STATE_RINGING.
-
- // TODO: Replace this with state transition to STATE_RINGING.
- for (Listener l : mListeners) {
- l.onSuccessfulIncomingCall(this);
- }
+ shouldSendToVoicemail = false;
+ }
+ // TODO: Make this class (not CallsManager) responsible for changing
+ // the call state to STATE_RINGING.
+ // TODO: Replace this with state transition to STATE_RINGING.
+ for (Listener l : mListeners) {
+ l.onSuccessfulIncomingCall(this, shouldSendToVoicemail);
}
mDirectToVoicemailQueryPending = false;
@@ -903,7 +1027,13 @@
* @param phoneAccountRegistrar The phone account registrar.
*/
void startCreateConnection(PhoneAccountRegistrar phoneAccountRegistrar) {
- Preconditions.checkState(mCreateConnectionProcessor == null);
+ if (mCreateConnectionProcessor != null) {
+ Log.w(this, "mCreateConnectionProcessor in startCreateConnection is not null. This is" +
+ " due to a race between NewOutgoingCallIntentBroadcaster and " +
+ "phoneAccountSelected, but is harmlessly resolved by ignoring the second " +
+ "invocation.");
+ return;
+ }
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
phoneAccountRegistrar, mContext);
mCreateConnectionProcessor.process();
@@ -931,26 +1061,37 @@
mConferenceableCalls.add(idMapper.getCall(id));
}
- if (mIsUnknown) {
- for (Listener l : mListeners) {
- l.onSuccessfulUnknownCall(this, getStateFromConnectionState(connection.getState()));
- }
- } else if (mIsIncoming) {
- // We do not handle incoming calls immediately when they are verified by the connection
- // service. We allow the caller-info-query code to execute first so that we can read the
- // direct-to-voicemail property before deciding if we want to show the incoming call to
- // the user or if we want to reject the call.
- mDirectToVoicemailQueryPending = true;
+ switch (mCallDirection) {
+ case CALL_DIRECTION_INCOMING:
+ // We do not handle incoming calls immediately when they are verified by the
+ // connection service. We allow the caller-info-query code to execute first so
+ // that we can read the direct-to-voicemail property before deciding if we want
+ // to show the incoming call to the user or if we want to reject the call.
+ mDirectToVoicemailQueryPending = true;
- // Timeout the direct-to-voicemail lookup execution so that we dont wait too long before
- // showing the user the incoming call screen.
- mHandler.postDelayed(mDirectToVoicemailRunnable, Timeouts.getDirectToVoicemailMillis(
- mContext.getContentResolver()));
- } else {
- for (Listener l : mListeners) {
- l.onSuccessfulOutgoingCall(this,
- getStateFromConnectionState(connection.getState()));
- }
+ // Timeout the direct-to-voicemail lookup execution so that we dont wait too long
+ // before showing the user the incoming call screen.
+ mHandler.postDelayed(new Runnable("C.hCCS") {
+ @Override
+ public void loggedRun() {
+ synchronized (mLock) {
+ processDirectToVoicemail();
+ }
+ }
+ }.prepare(), Timeouts.getDirectToVoicemailMillis(mContext.getContentResolver()));
+ break;
+ case CALL_DIRECTION_OUTGOING:
+ for (Listener l : mListeners) {
+ l.onSuccessfulOutgoingCall(this,
+ getStateFromConnectionState(connection.getState()));
+ }
+ break;
+ case CALL_DIRECTION_UNKNOWN:
+ for (Listener l : mListeners) {
+ l.onSuccessfulUnknownCall(this, getStateFromConnectionState(connection
+ .getState()));
+ }
+ break;
}
}
@@ -960,18 +1101,22 @@
setDisconnectCause(disconnectCause);
mCallsManager.markCallAsDisconnected(this, disconnectCause);
- if (mIsUnknown) {
- for (Listener listener : mListeners) {
- listener.onFailedUnknownCall(this);
- }
- } else if (mIsIncoming) {
- for (Listener listener : mListeners) {
- listener.onFailedIncomingCall(this);
- }
- } else {
- for (Listener listener : mListeners) {
- listener.onFailedOutgoingCall(this, disconnectCause);
- }
+ switch (mCallDirection) {
+ case CALL_DIRECTION_INCOMING:
+ for (Listener listener : mListeners) {
+ listener.onFailedIncomingCall(this);
+ }
+ break;
+ case CALL_DIRECTION_OUTGOING:
+ for (Listener listener : mListeners) {
+ listener.onFailedOutgoingCall(this, disconnectCause);
+ }
+ break;
+ case CALL_DIRECTION_UNKNOWN:
+ for (Listener listener : mListeners) {
+ listener.onFailedUnknownCall(this);
+ }
+ break;
}
}
@@ -1001,14 +1146,29 @@
}
}
- void disconnect() {
+ /**
+ * Silences the ringer.
+ */
+ void silence() {
+ if (mConnectionService == null) {
+ Log.w(this, "silence() request on a call without a connection service.");
+ } else {
+ Log.i(this, "Send silence to connection service for call %s", this);
+ Log.event(this, Log.Events.SILENCE);
+ mConnectionService.silence(this);
+ }
+ }
+
+ @VisibleForTesting
+ public void disconnect() {
disconnect(false);
}
/**
* Attempts to disconnect the call through the connection service.
*/
- void disconnect(boolean wasViaNewOutgoingCallBroadcaster) {
+ @VisibleForTesting
+ public void disconnect(boolean wasViaNewOutgoingCallBroadcaster) {
Log.event(this, Log.Events.REQUEST_DISCONNECT);
// Track that the call is now locally disconnecting.
@@ -1069,7 +1229,8 @@
*
* @param videoState The video state in which to answer the call.
*/
- void answer(int videoState) {
+ @VisibleForTesting
+ public void answer(int videoState) {
Preconditions.checkNotNull(mConnectionService);
// Check to verify that the call is still in the ringing state. A call can change states
@@ -1090,7 +1251,8 @@
* @param rejectWithMessage Whether to send a text message as part of the call rejection.
* @param textMessage An optional text message to send as part of the rejection.
*/
- void reject(boolean rejectWithMessage, String textMessage) {
+ @VisibleForTesting
+ public void reject(boolean rejectWithMessage, String textMessage) {
Preconditions.checkNotNull(mConnectionService);
// Check to verify that the call is still in the ringing state. A call can change states
@@ -1129,7 +1291,8 @@
}
/** Checks if this is a live call or not. */
- boolean isAlive() {
+ @VisibleForTesting
+ public boolean isAlive() {
switch (mState) {
case CallState.NEW:
case CallState.RINGING:
@@ -1167,7 +1330,8 @@
/**
* @return the uri of the contact associated with this call.
*/
- Uri getContactUri() {
+ @VisibleForTesting
+ public Uri getContactUri() {
if (mCallerInfo == null || !mCallerInfo.contactExists) {
return getHandle();
}
@@ -1212,7 +1376,8 @@
}
}
- void mergeConference() {
+ @VisibleForTesting
+ public void mergeConference() {
if (mConnectionService == null) {
Log.w(this, "merging conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
@@ -1222,7 +1387,8 @@
}
}
- void swapConference() {
+ @VisibleForTesting
+ public void swapConference() {
if (mConnectionService == null) {
Log.w(this, "swapping conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
@@ -1280,11 +1446,13 @@
}
}
- List<Call> getConferenceableCalls() {
+ @VisibleForTesting
+ public List<Call> getConferenceableCalls() {
return mConferenceableCalls;
}
- boolean can(int capability) {
+ @VisibleForTesting
+ public boolean can(int capability) {
return (mConnectionCapabilities & capability) == capability;
}
@@ -1411,17 +1579,27 @@
mCallerInfo = null;
if (!TextUtils.isEmpty(number)) {
Log.v(this, "Looking up information for: %s.", Log.piiHandle(number));
- mHandler.post(new Runnable() {
+ mHandler.post(new Runnable("C.sCIL") {
@Override
- public void run() {
- mCallerInfoAsyncQueryFactory.startQuery(
- mQueryToken,
- mContext,
- number,
- mCallerInfoQueryListener,
- Call.this);
+ public void loggedRun() {
+ Session subsubsession = null;
+ try {
+ subsubsession = Log.createSubsession();
+ CallerInfoAsyncQuery value = mCallerInfoAsyncQueryFactory.startQuery(
+ mQueryToken, mContext, number, mCallerInfoQueryListener,
+ new CallSessionCookie(Call.this, subsubsession));
+ // If there is an exception in startQuery, then this assignment will never
+ // occur.
+ if (value != null) {
+ subsubsession = null;
+ }
+ } finally {
+ if (subsubsession != null) {
+ Log.cancelSubsession(subsubsession);
+ }
+ }
}
- });
+ }.prepare());
}
}
@@ -1441,15 +1619,25 @@
Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
if (mCallerInfo.contactDisplayPhotoUri != null) {
- Log.d(this, "Searching person uri %s for call %s",
- mCallerInfo.contactDisplayPhotoUri, this);
- mContactsAsyncHelper.startObtainPhotoAsync(
- token,
- mContext,
- mCallerInfo.contactDisplayPhotoUri,
- mPhotoLoadListener,
- this);
- // Do not call onCallerInfoChanged yet in this case. We call it in setPhoto().
+ Session subsession = null;
+ try {
+ subsession = Log.createSubsession();
+ Log.d(this, "Searching person uri %s for call %s",
+ mCallerInfo.contactDisplayPhotoUri, this);
+ mContactsAsyncHelper.startObtainPhotoAsync(
+ token,
+ mContext,
+ mCallerInfo.contactDisplayPhotoUri,
+ mPhotoLoadListener,
+ new CallSessionCookie(this, subsession));
+ // If there is an exception, then this assignment will never occur.
+ subsession = null;
+ // Do not call onCallerInfoChanged yet in this case. We call it in setPhoto().
+ } finally {
+ if(subsession != null) {
+ Log.cancelSubsession(subsession);
+ }
+ }
} else {
for (Listener l : mListeners) {
l.onCallerInfoChanged(this);
@@ -1461,7 +1649,7 @@
Trace.endSection();
}
- CallerInfo getCallerInfo() {
+ public CallerInfo getCallerInfo() {
return mCallerInfo;
}
@@ -1484,7 +1672,9 @@
}
private void maybeLoadCannedSmsResponses() {
- if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) {
+ if (mCallDirection == CALL_DIRECTION_INCOMING
+ && isRespondViaSmsCapable()
+ && !mCannedSmsResponsesLoadingStarted) {
Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
mCannedSmsResponsesLoadingStarted = true;
mCallsManager.getRespondViaSmsManager().loadCannedTextMessages(
@@ -1636,11 +1826,7 @@
}
public boolean isUnknown() {
- return mIsUnknown;
- }
-
- public void setIsUnknown(boolean isUnknown) {
- mIsUnknown = isUnknown;
+ return mCallDirection == CALL_DIRECTION_UNKNOWN;
}
/**
@@ -1661,6 +1847,22 @@
mIsLocallyDisconnecting = isLocallyDisconnecting;
}
+ /**
+ * @return user handle of user initiating the outgoing call.
+ */
+ public UserHandle getInitiatingUser() {
+ return mInitiatingUser;
+ }
+
+ /**
+ * Set the user handle of user initiating the outgoing call.
+ * @param initiatingUser
+ */
+ public void setInitiatingUser(UserHandle initiatingUser) {
+ Preconditions.checkNotNull(initiatingUser);
+ mInitiatingUser = initiatingUser;
+ }
+
static int getStateFromConnectionState(int state) {
switch (state) {
case Connection.STATE_INITIALIZING:
@@ -1707,4 +1909,16 @@
public long getCallDataUsage() {
return mCallDataUsage;
}
+
+ /**
+ * Returns true if the call is outgoing and the NEW_OUTGOING_CALL ordered broadcast intent
+ * has come back to telecom and was processed.
+ */
+ public boolean isNewOutgoingCallIntentBroadcastDone() {
+ return mIsNewOutgoingCallIntentBroadcastDone;
+ }
+
+ public void setNewOutgoingCallIntentBroadcastIsDone() {
+ mIsNewOutgoingCallIntentBroadcastDone = true;
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 23284e3..1a5cc7b 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -11,331 +11,276 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
package com.android.server.telecom;
-import android.app.ActivityManagerNative;
-import android.content.Context;
-import android.content.pm.UserInfo;
-import android.media.AudioManager;
+import android.annotation.NonNull;
import android.media.IAudioService;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.UserHandle;
+import android.media.ToneGenerator;
import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
+import android.util.SparseArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.internal.util.Preconditions;
-import java.util.Objects;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.LinkedHashSet;
-/**
- * This class manages audio modes, streams and other properties.
- */
-final class CallAudioManager extends CallsManagerListenerBase
- implements WiredHeadsetManager.Listener, DockManager.Listener {
- private static final int STREAM_NONE = -1;
+public class CallAudioManager extends CallsManagerListenerBase {
- private static final String STREAM_DESCRIPTION_NONE = "STEAM_NONE";
- private static final String STREAM_DESCRIPTION_ALARM = "STEAM_ALARM";
- private static final String STREAM_DESCRIPTION_BLUETOOTH_SCO = "STREAM_BLUETOOTH_SCO";
- private static final String STREAM_DESCRIPTION_DTMF = "STREAM_DTMF";
- private static final String STREAM_DESCRIPTION_MUSIC = "STREAM_MUSIC";
- private static final String STREAM_DESCRIPTION_NOTIFICATION = "STREAM_NOTIFICATION";
- private static final String STREAM_DESCRIPTION_RING = "STREAM_RING";
- private static final String STREAM_DESCRIPTION_SYSTEM = "STREAM_SYSTEM";
- private static final String STREAM_DESCRIPTION_VOICE_CALL = "STREAM_VOICE_CALL";
+ public interface AudioServiceFactory {
+ IAudioService getAudioService();
+ }
- private static final String MODE_DESCRIPTION_INVALID = "MODE_INVALID";
- private static final String MODE_DESCRIPTION_CURRENT = "MODE_CURRENT";
- private static final String MODE_DESCRIPTION_NORMAL = "MODE_NORMAL";
- private static final String MODE_DESCRIPTION_RINGTONE = "MODE_RINGTONE";
- private static final String MODE_DESCRIPTION_IN_CALL = "MODE_IN_CALL";
- private static final String MODE_DESCRIPTION_IN_COMMUNICATION = "MODE_IN_COMMUNICATION";
+ private final String LOG_TAG = CallAudioManager.class.getSimpleName();
- private static final int MSG_AUDIO_MANAGER_INITIALIZE = 0;
- private static final int MSG_AUDIO_MANAGER_TURN_ON_SPEAKER = 1;
- private static final int MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL = 2;
- private static final int MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE = 3;
- private static final int MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL = 4;
- private static final int MSG_AUDIO_MANAGER_SET_MODE = 5;
+ private final LinkedHashSet<Call> mActiveOrDialingCalls;
+ private final LinkedHashSet<Call> mRingingCalls;
+ private final LinkedHashSet<Call> mHoldingCalls;
+ private final Set<Call> mCalls;
+ private final SparseArray<LinkedHashSet<Call>> mCallStateToCalls;
- private final Handler mAudioManagerHandler = new Handler(Looper.getMainLooper()) {
-
- private AudioManager mAudioManager;
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_AUDIO_MANAGER_INITIALIZE: {
- mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- break;
- }
- case MSG_AUDIO_MANAGER_TURN_ON_SPEAKER: {
- boolean on = (msg.arg1 != 0);
- // Wired headset and earpiece work the same way
- if (mAudioManager.isSpeakerphoneOn() != on) {
- Log.i(this, "turning speaker phone %s", on);
- mAudioManager.setSpeakerphoneOn(on);
- }
- break;
- }
- case MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL: {
- mAudioManager.abandonAudioFocusForCall();
- break;
- }
- case MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE: {
- boolean mute = (msg.arg1 != 0);
- if (mute != mAudioManager.isMicrophoneMute()) {
- IAudioService audio = getAudioService();
- Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
- mute, audio == null);
- if (audio != null) {
- try {
- // We use the audio service directly here so that we can specify
- // the current user. Telecom runs in the system_server process which
- // may run as a separate user from the foreground user. If we
- // used AudioManager directly, we would change mute for the system's
- // user and not the current foreground, which we want to avoid.
- audio.setMicrophoneMute(
- mute, mContext.getOpPackageName(), getCurrentUserId());
-
- } catch (RemoteException e) {
- Log.e(this, e, "Remote exception while toggling mute.");
- }
- // TODO: Check microphone state after attempting to set to ensure that
- // our state corroborates AudioManager's state.
- }
- }
-
- break;
- }
- case MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL: {
- int stream = msg.arg1;
- mAudioManager.requestAudioFocusForCall(
- stream,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
- break;
- }
- case MSG_AUDIO_MANAGER_SET_MODE: {
- int newMode = msg.arg1;
- int oldMode = mAudioManager.getMode();
- Log.v(this, "Request to change audio mode from %s to %s", modeToString(oldMode),
- modeToString(newMode));
-
- if (oldMode != newMode) {
- if (oldMode == AudioManager.MODE_IN_CALL &&
- newMode == AudioManager.MODE_RINGTONE) {
- Log.i(this, "Transition from IN_CALL -> RINGTONE."
- + " Resetting to NORMAL first.");
- mAudioManager.setMode(AudioManager.MODE_NORMAL);
- }
- mAudioManager.setMode(newMode);
- synchronized (mLock) {
- mMostRecentlyUsedMode = newMode;
- }
- }
- break;
- }
- default:
- break;
- }
- }
- };
-
- private final Context mContext;
- private final TelecomSystem.SyncRoot mLock;
- private final StatusBarNotifier mStatusBarNotifier;
- private final BluetoothManager mBluetoothManager;
- private final WiredHeadsetManager mWiredHeadsetManager;
- private final DockManager mDockManager;
+ private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ private final CallAudioModeStateMachine mCallAudioModeStateMachine;
private final CallsManager mCallsManager;
+ private final InCallTonePlayer.Factory mPlayerFactory;
+ private final Ringer mRinger;
+ private final RingbackPlayer mRingbackPlayer;
+ private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
- private CallAudioState mCallAudioState;
- private int mAudioFocusStreamType;
- private boolean mIsRinging;
- private boolean mIsTonePlaying;
- private boolean mWasSpeakerOn;
- private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL;
- private Call mCallToSpeedUpMTAudio = null;
+ private Call mForegroundCall;
+ private boolean mIsTonePlaying = false;
- CallAudioManager(
- Context context,
- TelecomSystem.SyncRoot lock,
- StatusBarNotifier statusBarNotifier,
- WiredHeadsetManager wiredHeadsetManager,
- DockManager dockManager,
- CallsManager callsManager) {
- mContext = context;
- mLock = lock;
- mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_INITIALIZE, 0, 0).sendToTarget();
- mStatusBarNotifier = statusBarNotifier;
- mBluetoothManager = new BluetoothManager(context, this);
- mWiredHeadsetManager = wiredHeadsetManager;
+ public CallAudioManager(CallAudioRouteStateMachine callAudioRouteStateMachine,
+ CallsManager callsManager,
+ CallAudioModeStateMachine callAudioModeStateMachine,
+ InCallTonePlayer.Factory playerFactory,
+ Ringer ringer,
+ RingbackPlayer ringbackPlayer,
+ DtmfLocalTonePlayer dtmfLocalTonePlayer) {
+ mActiveOrDialingCalls = new LinkedHashSet<>();
+ mRingingCalls = new LinkedHashSet<>();
+ mHoldingCalls = new LinkedHashSet<>();
+ mCalls = new HashSet<>();
+ mCallStateToCalls = new SparseArray<LinkedHashSet<Call>>() {{
+ put(CallState.ACTIVE, mActiveOrDialingCalls);
+ put(CallState.DIALING, mActiveOrDialingCalls);
+ put(CallState.RINGING, mRingingCalls);
+ put(CallState.ON_HOLD, mHoldingCalls);
+ }};
+
+ mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+ mCallAudioModeStateMachine = callAudioModeStateMachine;
mCallsManager = callsManager;
+ mPlayerFactory = playerFactory;
+ mRinger = ringer;
+ mRingbackPlayer = ringbackPlayer;
+ mDtmfLocalTonePlayer = dtmfLocalTonePlayer;
- mWiredHeadsetManager.addListener(this);
- mDockManager = dockManager;
- mDockManager.addListener(this);
-
- saveAudioState(getInitialAudioState(null));
- mAudioFocusStreamType = STREAM_NONE;
- }
-
- CallAudioState getCallAudioState() {
- return mCallAudioState;
- }
-
- @Override
- public void onCallAdded(Call call) {
- Log.v(this, "onCallAdded");
- onCallUpdated(call);
-
- if (hasFocus() && getForegroundCall() == call) {
- if (!call.isIncoming()) {
- // Unmute new outgoing call.
- setSystemAudioState(false, mCallAudioState.getRoute(),
- mCallAudioState.getSupportedRouteMask());
- }
- }
- }
-
- @Override
- public void onCallRemoved(Call call) {
- Log.v(this, "onCallRemoved");
- // If we didn't already have focus, there's nothing to do.
- if (hasFocus()) {
- if (mCallsManager.getCalls().isEmpty()) {
- Log.v(this, "all calls removed, resetting system audio to default state");
- setInitialAudioState(null, false /* force */);
- mWasSpeakerOn = false;
- }
- updateAudioStreamAndMode(call);
- }
+ mPlayerFactory.setCallAudioManager(this);
+ mCallAudioModeStateMachine.setCallAudioManager(this);
}
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
- Log.v(this, "onCallStateChanged : oldState = %d, newState = %d", oldState, newState);
- onCallUpdated(call);
+ if (call.getParentCall() != null) {
+ // No audio management for calls in a conference.
+ return;
+ }
+ Log.d(LOG_TAG, "Call state changed for TC@%s: %s -> %s", call.getId(),
+ CallState.toString(oldState), CallState.toString(newState));
+
+ if (mCallStateToCalls.get(oldState) != null) {
+ mCallStateToCalls.get(oldState).remove(call);
+ }
+ if (mCallStateToCalls.get(newState) != null) {
+ mCallStateToCalls.get(newState).add(call);
+ }
+
+ updateForegroundCall();
+ if (newState == CallState.DISCONNECTED) {
+ playToneForDisconnectedCall(call);
+ }
+
+ onCallLeavingState(call, oldState);
+ onCallEnteringState(call, newState);
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (call.getParentCall() != null) {
+ return; // Don't do audio handling for calls in a conference.
+ }
+
+ if (mCalls.contains(call)) {
+ Log.w(LOG_TAG, "Call TC@%s is being added twice.", call.getId());
+ return; // No guarantees that the same call won't get added twice.
+ }
+
+ Log.d(LOG_TAG, "Call added with id TC@%s in state %s", call.getId(),
+ CallState.toString(call.getState()));
+
+ if (mCallStateToCalls.get(call.getState()) != null) {
+ mCallStateToCalls.get(call.getState()).add(call);
+ }
+ updateForegroundCall();
+ mCalls.add(call);
+
+ onCallEnteringState(call, call.getState());
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ if (call.getParentCall() != null) {
+ return; // Don't do audio handling for calls in a conference.
+ }
+
+ if (!mCalls.contains(call)) {
+ return; // No guarantees that the same call won't get removed twice.
+ }
+
+ Log.d(LOG_TAG, "Call removed with id TC@%s in state %s", call.getId(),
+ CallState.toString(call.getState()));
+
+ if (mCallStateToCalls.get(call.getState()) != null) {
+ mCallStateToCalls.get(call.getState()).remove(call);
+ }
+
+ updateForegroundCall();
+ mCalls.remove(call);
+
+ onCallLeavingState(call, call.getState());
+
+ if (mCallsManager.getCalls().isEmpty()) {
+ Log.v(this, "all calls removed, resetting system audio to default state");
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.REINITIALIZE);
+ }
}
@Override
public void onIncomingCallAnswered(Call call) {
- Log.v(this, "onIncomingCallAnswered");
- int route = mCallAudioState.getRoute();
-
- // We do two things:
- // (1) If this is the first call, then we can to turn on bluetooth if available.
- // (2) Unmute the audio for the new incoming call.
- boolean isOnlyCall = mCallsManager.getCalls().size() == 1;
- if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
- mBluetoothManager.connectBluetoothAudio();
- route = CallAudioState.ROUTE_BLUETOOTH;
- }
-
- setSystemAudioState(false /* isMute */, route, mCallAudioState.getSupportedRouteMask());
+ // This is called after the UI answers the call, but before the connection service
+ // sets the call to active. Only thing to handle for mode here is the audio speedup thing.
if (call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO)) {
- Log.v(this, "Speed up audio setup for IMS MT call.");
- mCallToSpeedUpMTAudio = call;
- updateAudioStreamAndMode(call);
+ if (mForegroundCall == call) {
+ Log.i(LOG_TAG, "Invoking the MT_AUDIO_SPEEDUP mechanism. Transitioning into " +
+ "an active in-call audio state before connection service has " +
+ "connected the call.");
+ if (mCallStateToCalls.get(call.getState()) != null) {
+ mCallStateToCalls.get(call.getState()).remove(call);
+ }
+ mActiveOrDialingCalls.add(call);
+ mCallAudioModeStateMachine.sendMessage(
+ CallAudioModeStateMachine.MT_AUDIO_SPEEDUP_FOR_RINGING_CALL,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ if (mRingingCalls.size() == 0) {
+ mRinger.stopRinging();
+ mRinger.stopCallWaiting();
}
}
@Override
- public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
- onCallUpdated(newForegroundCall);
- // Ensure that the foreground call knows about the latest audio state.
- updateAudioForForegroundCall();
+ public void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile) {
+ if (videoProfile == null) {
+ return;
+ }
+
+ if (call != mForegroundCall) {
+ // We only play tones for foreground calls.
+ return;
+ }
+
+ int previousVideoState = call.getVideoState();
+ int newVideoState = videoProfile.getVideoState();
+ Log.v(this, "onSessionModifyRequestReceived : videoProfile = " + VideoProfile
+ .videoStateToString(newVideoState));
+
+ boolean isUpgradeRequest = !VideoProfile.isReceptionEnabled(previousVideoState) &&
+ VideoProfile.isReceptionEnabled(newVideoState);
+
+ if (isUpgradeRequest) {
+ mPlayerFactory.createPlayer(InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
+ }
}
@Override
public void onIsVoipAudioModeChanged(Call call) {
- updateAudioStreamAndMode(call);
- }
-
- /**
- * Updates the audio route when the headset plugged in state changes. For example, if audio is
- * being routed over speakerphone and a headset is plugged in then switch to wired headset.
- */
- @Override
- public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
+ if (call != mForegroundCall) {
return;
}
-
- boolean isCurrentlyWiredHeadset = mCallAudioState.getRoute()
- == CallAudioState.ROUTE_WIRED_HEADSET;
-
- int newRoute = mCallAudioState.getRoute(); // start out with existing route
- if (newIsPluggedIn) {
- newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
- } else if (isCurrentlyWiredHeadset) {
- Call call = getForegroundCall();
- boolean hasLiveCall = call != null && call.isAlive();
-
- if (hasLiveCall) {
- // In order of preference when a wireless headset is unplugged.
- if (mWasSpeakerOn) {
- newRoute = CallAudioState.ROUTE_SPEAKER;
- } else {
- newRoute = CallAudioState.ROUTE_EARPIECE;
- }
-
- // We don't automatically connect to bluetooth when user unplugs their wired headset
- // and they were previously using the wired. Wired and earpiece are effectively the
- // same choice in that they replace each other as an option when wired headsets
- // are plugged in and out. This means that keeping it earpiece is a bit more
- // consistent with the status quo. Bluetooth also has more danger associated with
- // choosing it in the wrong curcumstance because bluetooth devices can be
- // semi-public (like in a very-occupied car) where earpiece doesn't carry that risk.
- }
- }
-
- // We need to call this every time even if we do not change the route because the supported
- // routes changed either to include or not include WIRED_HEADSET.
- setSystemAudioState(mCallAudioState.isMuted(), newRoute, calculateSupportedRoutes());
+ mCallAudioModeStateMachine.sendMessage(
+ CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
+ makeArgsForModeStateMachine());
}
@Override
- public void onDockChanged(boolean isDocked) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
- if (isDocked) {
- // Device just docked, turn to speakerphone. Only do so if the route is currently
- // earpiece so that we dont switch out of a BT headset or a wired headset.
- if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE) {
- setAudioRoute(CallAudioState.ROUTE_SPEAKER);
- }
+ public void onRingbackRequested(Call call, boolean shouldRingback) {
+ if (call == mForegroundCall && shouldRingback) {
+ mRingbackPlayer.startRingbackForCall(call);
} else {
- // Device just undocked, remove from speakerphone if possible.
- if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
- setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
- }
+ mRingbackPlayer.stopRingbackForCall(call);
}
}
+ @Override
+ public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String message) {
+ // This gets called after the UI rejects a call but before the CS processes the rejection.
+ // Will get called before the state change from ringing to not ringing.
+
+ if (mRingingCalls.size() == 0 || call == mRingingCalls.iterator().next()) {
+ mRinger.stopRinging();
+ mRinger.stopCallWaiting();
+ }
+ }
+
+ @Override
+ public void onIsConferencedChanged(Call call) {
+ // This indicates a conferencing change, which shouldn't impact any audio mode stuff.
+ Call parentCall = call.getParentCall();
+ if (parentCall == null) {
+ // Indicates that the call should be tracked for audio purposes. Treat it as if it were
+ // just added.
+ Log.i(LOG_TAG, "Call TC@" + call.getId() + " left conference and will" +
+ " now be tracked by CallAudioManager.");
+ onCallAdded(call);
+ } else {
+ // The call joined a conference, so stop tracking it.
+ if (mCallStateToCalls.get(call.getState()) != null) {
+ mCallStateToCalls.get(call.getState()).remove(call);
+ }
+
+ updateForegroundCall();
+ mCalls.remove(call);
+ }
+ }
+
+ public CallAudioState getCallAudioState() {
+ return mCallAudioRouteStateMachine.getCurrentCallAudioState();
+ }
+
+ public Call getForegroundCall() {
+ if (mForegroundCall != null && mForegroundCall.getState() != CallState.ON_HOLD) {
+ return mForegroundCall;
+ }
+ return null;
+ }
+
void toggleMute() {
- mute(!mCallAudioState.isMuted());
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.TOGGLE_MUTE);
}
void mute(boolean shouldMute) {
- if (!hasFocus()) {
- return;
- }
-
Log.v(this, "mute, shouldMute: %b", shouldMute);
// Don't mute if there are any emergency calls.
@@ -344,15 +289,8 @@
Log.v(this, "ignoring mute for emergency call");
}
- if (mCallAudioState.isMuted() != shouldMute) {
- // We user CallsManager's foreground call so that we dont ignore ringing calls
- // for logging purposes
- Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
- shouldMute ? "on" : "off");
-
- setSystemAudioState(shouldMute, mCallAudioState.getRoute(),
- mCallAudioState.getSupportedRouteMask());
- }
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(shouldMute
+ ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
}
/**
@@ -361,468 +299,264 @@
* @param route The new audio route to use. See {@link CallAudioState}.
*/
void setAudioRoute(int route) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
-
- // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
- int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
-
- // If route is unsupported, do nothing.
- if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
- Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
- return;
- }
-
- if (mCallAudioState.getRoute() != newRoute) {
- // Remember the new speaker state so it can be restored when the user plugs and unplugs
- // a headset.
- mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
- setSystemAudioState(mCallAudioState.isMuted(), newRoute,
- mCallAudioState.getSupportedRouteMask());
- }
- }
-
- /**
- * Sets the audio stream and mode based on whether a call is ringing.
- *
- * @param call The call which changed ringing state.
- * @param isRinging {@code true} if the call is ringing, {@code false} otherwise.
- */
- void setIsRinging(Call call, boolean isRinging) {
- if (mIsRinging != isRinging) {
- Log.i(this, "setIsRinging %b -> %b (call = %s)", mIsRinging, isRinging, call);
- mIsRinging = isRinging;
- updateAudioStreamAndMode(call);
- }
- }
-
- /**
- * Sets the tone playing status. Some tones can play even when there are no live calls and this
- * status indicates that we should keep audio focus even for tones that play beyond the life of
- * calls.
- *
- * @param isPlayingNew The status to set.
- */
- void setIsTonePlaying(boolean isPlayingNew) {
- if (mIsTonePlaying != isPlayingNew) {
- Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
- mIsTonePlaying = isPlayingNew;
- updateAudioStreamAndMode();
- }
- }
-
- /**
- * Updates the audio routing according to the bluetooth state.
- */
- void onBluetoothStateChange(BluetoothManager bluetoothManager) {
- // This can happen even when there are no calls and we don't have focus.
- if (!hasFocus()) {
- return;
- }
-
- int supportedRoutes = calculateSupportedRoutes();
- int newRoute = mCallAudioState.getRoute();
- if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
- newRoute = CallAudioState.ROUTE_BLUETOOTH;
- } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
- newRoute = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE,
- supportedRoutes);
- // Do not switch to speaker when bluetooth disconnects.
- mWasSpeakerOn = false;
- }
-
- setSystemAudioState(mCallAudioState.isMuted(), newRoute, supportedRoutes);
- }
-
- boolean isBluetoothAudioOn() {
- return mBluetoothManager.isBluetoothAudioConnected();
- }
-
- boolean isBluetoothDeviceAvailable() {
- return mBluetoothManager.isBluetoothAvailable();
- }
-
- private void saveAudioState(CallAudioState callAudioState) {
- mCallAudioState = callAudioState;
- mStatusBarNotifier.notifyMute(mCallAudioState.isMuted());
- mStatusBarNotifier.notifySpeakerphone(mCallAudioState.getRoute()
- == CallAudioState.ROUTE_SPEAKER);
- }
-
- private void onCallUpdated(Call call) {
- updateAudioStreamAndMode(call);
- if (call != null && call.getState() == CallState.ACTIVE &&
- call == mCallToSpeedUpMTAudio) {
- mCallToSpeedUpMTAudio = null;
- }
- }
-
- private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
- setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);
- }
-
- private void setSystemAudioState(
- boolean force, boolean isMuted, int route, int supportedRouteMask) {
- if (!hasFocus()) {
- return;
- }
-
- CallAudioState oldAudioState = mCallAudioState;
- saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
- if (!force && Objects.equals(oldAudioState, mCallAudioState)) {
- return;
- }
-
- Log.i(this, "setSystemAudioState: changing from %s to %s", oldAudioState, mCallAudioState);
- Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
- CallAudioState.audioRouteToString(mCallAudioState.getRoute()));
-
- mAudioManagerHandler.obtainMessage(
- MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE,
- mCallAudioState.isMuted() ? 1 : 0,
- 0)
- .sendToTarget();
-
- // Audio route.
- if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
- turnOnSpeaker(false);
- turnOnBluetooth(true);
- } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
- turnOnBluetooth(false);
- turnOnSpeaker(true);
- } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||
- mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
- turnOnBluetooth(false);
- turnOnSpeaker(false);
- }
-
- if (!oldAudioState.equals(mCallAudioState)) {
- mCallsManager.onCallAudioStateChanged(oldAudioState, mCallAudioState);
- updateAudioForForegroundCall();
- }
- }
-
- private void turnOnSpeaker(boolean on) {
- mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_TURN_ON_SPEAKER, on ? 1 : 0, 0)
- .sendToTarget();
- }
-
- private void turnOnBluetooth(boolean on) {
- if (mBluetoothManager.isBluetoothAvailable()) {
- boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
- if (on != isAlreadyOn) {
- Log.i(this, "connecting bluetooth %s", on);
- if (on) {
- mBluetoothManager.connectBluetoothAudio();
- } else {
- mBluetoothManager.disconnectBluetoothAudio();
- }
- }
- }
- }
-
- private void updateAudioStreamAndMode() {
- updateAudioStreamAndMode(null /* call */);
- }
-
- private void updateAudioStreamAndMode(Call callToUpdate) {
- Log.i(this, "updateAudioStreamAndMode : mIsRinging: %b, mIsTonePlaying: %b, call: %s",
- mIsRinging, mIsTonePlaying, callToUpdate);
-
- boolean wasVoiceCall = mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL;
- if (mIsRinging) {
- Log.i(this, "updateAudioStreamAndMode : ringing");
- requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
- } else {
- Call foregroundCall = getForegroundCall();
- Call waitingForAccountSelectionCall = mCallsManager
- .getFirstCallWithState(CallState.SELECT_PHONE_ACCOUNT);
- Call call = mCallsManager.getForegroundCall();
- if (foregroundCall == null && call != null && call == mCallToSpeedUpMTAudio) {
- Log.v(this, "updateAudioStreamAndMode : no foreground, speeding up MT audio.");
- requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL,
- AudioManager.MODE_IN_CALL);
- } else if (foregroundCall != null && !foregroundCall.isDisconnected() &&
- waitingForAccountSelectionCall == null) {
- // In the case where there is a call that is waiting for account selection,
- // this will fall back to abandonAudioFocus() below, which temporarily exits
- // the in-call audio mode. This is to allow TalkBack to speak the "Call with"
- // dialog information at media volume as opposed to through the earpiece.
- // Once exiting the "Call with" dialog, the audio focus will return to an in-call
- // audio mode when this method (updateAudioStreamAndMode) is called again.
- int mode = foregroundCall.getIsVoipAudioMode() ?
- AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_IN_CALL;
- Log.v(this, "updateAudioStreamAndMode : foreground");
- requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
- } else if (mIsTonePlaying) {
- // There is no call, however, we are still playing a tone, so keep focus.
- // Since there is no call from which to determine the mode, use the most
- // recently used mode instead.
- Log.v(this, "updateAudioStreamAndMode : tone playing");
- requestAudioFocusAndSetMode(
- AudioManager.STREAM_VOICE_CALL, mMostRecentlyUsedMode);
- } else if (!hasRingingForegroundCall() && mCallsManager.hasOnlyDisconnectedCalls()) {
- Log.v(this, "updateAudioStreamAndMode : no ringing call");
- abandonAudioFocus();
- } else {
- // mIsRinging is false, but there is a foreground ringing call present. Don't
- // abandon audio focus immediately to prevent audio focus from getting lost between
- // the time it takes for the foreground call to transition from RINGING to ACTIVE/
- // DISCONNECTED. When the call eventually transitions to the next state, audio
- // focus will be correctly abandoned by the if clause above.
- }
- }
-
- boolean isVoiceCall = mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL;
-
- // If we transition from not a voice call to a voice call, we need to set an initial audio
- // state for the call.
- if (!wasVoiceCall && isVoiceCall) {
- setInitialAudioState(callToUpdate, true /* force */);
- }
- }
-
- private void requestAudioFocusAndSetMode(int stream, int mode) {
- Log.v(this, "requestAudioFocusAndSetMode : stream: %s -> %s, mode: %s",
- streamTypeToString(mAudioFocusStreamType), streamTypeToString(stream),
- modeToString(mode));
- Preconditions.checkState(stream != STREAM_NONE);
-
- // Even if we already have focus, if the stream is different we update audio manager to give
- // it a hint about the purpose of our focus.
- if (mAudioFocusStreamType != stream) {
- Log.i(this, "requestAudioFocusAndSetMode : requesting stream: %s -> %s",
- streamTypeToString(mAudioFocusStreamType), streamTypeToString(stream));
- mAudioManagerHandler.obtainMessage(
- MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL,
- stream,
- 0)
- .sendToTarget();
- }
- mAudioFocusStreamType = stream;
-
- setMode(mode);
- }
-
- private void abandonAudioFocus() {
- if (hasFocus()) {
- setMode(AudioManager.MODE_NORMAL);
- Log.v(this, "abandoning audio focus");
- mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL, 0, 0)
- .sendToTarget();
- mAudioFocusStreamType = STREAM_NONE;
- mCallToSpeedUpMTAudio = null;
- }
- }
-
- /**
- * Sets the audio mode.
- *
- * @param newMode Mode constant from AudioManager.MODE_*.
- */
- private void setMode(int newMode) {
- Preconditions.checkState(hasFocus());
- mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_SET_MODE, newMode, 0).sendToTarget();
- }
-
- private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
- // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
- // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
- // supported before calling setAudioRoute.
- if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
- route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
- if (route == 0) {
- Log.wtf(this, "One of wired headset or earpiece should always be valid.");
- // assume earpiece in this case.
- route = CallAudioState.ROUTE_EARPIECE;
- }
- }
- return route;
- }
-
- private int calculateSupportedRoutes() {
- int routeMask = CallAudioState.ROUTE_SPEAKER;
-
- if (mWiredHeadsetManager.isPluggedIn()) {
- routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
- } else {
- routeMask |= CallAudioState.ROUTE_EARPIECE;
- }
-
- if (mBluetoothManager.isBluetoothAvailable()) {
- routeMask |= CallAudioState.ROUTE_BLUETOOTH;
- }
-
- return routeMask;
- }
-
- private CallAudioState getInitialAudioState(Call call) {
- int supportedRouteMask = calculateSupportedRoutes();
- int route = selectWiredOrEarpiece(
- CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
-
- // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
- // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
- // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
- // *will* be routed to a bluetooth headset once the call is answered. In this case, just
- // check if the headset is available. Note this only applies when we are dealing with
- // the first call.
- if (call != null && mBluetoothManager.isBluetoothAvailable()) {
- switch(call.getState()) {
- case CallState.ACTIVE:
- case CallState.ON_HOLD:
- case CallState.DIALING:
- case CallState.CONNECTING:
- case CallState.RINGING:
- route = CallAudioState.ROUTE_BLUETOOTH;
- break;
- default:
- break;
- }
- }
-
- return new CallAudioState(false, route, supportedRouteMask);
- }
-
- private void setInitialAudioState(Call call, boolean force) {
- CallAudioState audioState = getInitialAudioState(call);
- Log.i(this, "setInitialAudioState : audioState = %s, call = %s", audioState, call);
- setSystemAudioState(
- force, audioState.isMuted(), audioState.getRoute(),
- audioState.getSupportedRouteMask());
- }
-
- private void updateAudioForForegroundCall() {
- Call call = mCallsManager.getForegroundCall();
- if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onCallAudioStateChanged(call, mCallAudioState);
- }
- }
-
- /**
- * Returns the current foreground call in order to properly set the audio mode.
- */
- private Call getForegroundCall() {
- Call call = mCallsManager.getForegroundCall();
-
- // We ignore any foreground call that is in the ringing state because we deal with ringing
- // calls exclusively through the mIsRinging variable set by {@link Ringer}.
- if (call != null && call.getState() == CallState.RINGING) {
- return null;
- }
-
- return call;
- }
-
- private boolean hasRingingForegroundCall() {
- Call call = mCallsManager.getForegroundCall();
- return call != null && call.getState() == CallState.RINGING;
- }
-
- private boolean hasFocus() {
- return mAudioFocusStreamType != STREAM_NONE;
- }
-
- private IAudioService getAudioService() {
- return IAudioService.Stub.asInterface(ServiceManager.getService(Context.AUDIO_SERVICE));
- }
-
- private int getCurrentUserId() {
- final long ident = Binder.clearCallingIdentity();
- try {
- UserInfo currentUser = ActivityManagerNative.getDefault().getCurrentUser();
- return currentUser.id;
- } catch (RemoteException e) {
- // Activity manager not running, nothing we can do assume user 0.
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- return UserHandle.USER_OWNER;
- }
-
- /**
- * Translates an {@link AudioManager} stream type to a human-readable string description.
- *
- * @param streamType The stream type.
- * @return Human readable description.
- */
- private String streamTypeToString(int streamType) {
- switch (streamType) {
- case STREAM_NONE:
- return STREAM_DESCRIPTION_NONE;
- case AudioManager.STREAM_ALARM:
- return STREAM_DESCRIPTION_ALARM;
- case AudioManager.STREAM_BLUETOOTH_SCO:
- return STREAM_DESCRIPTION_BLUETOOTH_SCO;
- case AudioManager.STREAM_DTMF:
- return STREAM_DESCRIPTION_DTMF;
- case AudioManager.STREAM_MUSIC:
- return STREAM_DESCRIPTION_MUSIC;
- case AudioManager.STREAM_NOTIFICATION:
- return STREAM_DESCRIPTION_NOTIFICATION;
- case AudioManager.STREAM_RING:
- return STREAM_DESCRIPTION_RING;
- case AudioManager.STREAM_SYSTEM:
- return STREAM_DESCRIPTION_SYSTEM;
- case AudioManager.STREAM_VOICE_CALL:
- return STREAM_DESCRIPTION_VOICE_CALL;
+ switch (route) {
+ case CallAudioState.ROUTE_BLUETOOTH:
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH);
+ return;
+ case CallAudioState.ROUTE_SPEAKER:
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_SPEAKER);
+ return;
+ case CallAudioState.ROUTE_WIRED_HEADSET:
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_HEADSET);
+ return;
+ case CallAudioState.ROUTE_EARPIECE:
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_EARPIECE);
+ return;
+ case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_BASELINE_ROUTE);
+ return;
default:
- return "STEAM_OTHER_" + streamType;
+ Log.wtf(this, "Invalid route specified: %d", route);
}
}
- /**
- * Translates an {@link AudioManager} mode into a human readable string.
- *
- * @param mode The mode.
- * @return The string.
- */
- private String modeToString(int mode) {
- switch (mode) {
- case AudioManager.MODE_INVALID:
- return MODE_DESCRIPTION_INVALID;
- case AudioManager.MODE_CURRENT:
- return MODE_DESCRIPTION_CURRENT;
- case AudioManager.MODE_NORMAL:
- return MODE_DESCRIPTION_NORMAL;
- case AudioManager.MODE_RINGTONE:
- return MODE_DESCRIPTION_RINGTONE;
- case AudioManager.MODE_IN_CALL:
- return MODE_DESCRIPTION_IN_CALL;
- case AudioManager.MODE_IN_COMMUNICATION:
- return MODE_DESCRIPTION_IN_COMMUNICATION;
- default:
- return "MODE_OTHER_" + mode;
+ void silenceRingers() {
+ for (Call call : mRingingCalls) {
+ call.silence();
}
+
+ mRingingCalls.clear();
+ mRinger.stopRinging();
+ mRinger.stopCallWaiting();
+ mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
+ makeArgsForModeStateMachine());
}
- /**
- * Dumps the state of the {@link CallAudioManager}.
- *
- * @param pw The {@code IndentingPrintWriter} to write the state to.
- */
- public void dump(IndentingPrintWriter pw) {
- pw.println("mAudioState: " + mCallAudioState);
- pw.println("mBluetoothManager:");
+ @VisibleForTesting
+ public void startRinging() {
+ mRinger.startRinging(mForegroundCall);
+ }
+
+ @VisibleForTesting
+ public void startCallWaiting() {
+ mRinger.startCallWaiting(mRingingCalls.iterator().next());
+ }
+
+ @VisibleForTesting
+ public void stopRinging() {
+ mRinger.stopRinging();
+ }
+
+ @VisibleForTesting
+ public void stopCallWaiting() {
+ mRinger.stopCallWaiting();
+ }
+
+ @VisibleForTesting
+ public void setCallAudioRouteFocusState(int focusState) {
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Active or dialing calls:");
pw.increaseIndent();
- mBluetoothManager.dump(pw);
+ dumpCallsInCollection(pw, mActiveOrDialingCalls);
pw.decreaseIndent();
- if (mWiredHeadsetManager != null) {
- pw.println("mWiredHeadsetManager:");
- pw.increaseIndent();
- mWiredHeadsetManager.dump(pw);
- pw.decreaseIndent();
- } else {
- pw.println("mWiredHeadsetManager: null");
- }
- pw.println("mAudioFocusStreamType: " + streamTypeToString(mAudioFocusStreamType));
- pw.println("mIsRinging: " + mIsRinging);
- pw.println("mIsTonePlaying: " + mIsTonePlaying);
- pw.println("mWasSpeakerOn: " + mWasSpeakerOn);
- pw.println("mMostRecentlyUsedMode: " + modeToString(mMostRecentlyUsedMode));
+
+ pw.println("Ringing calls:");
+ pw.increaseIndent();
+ dumpCallsInCollection(pw, mRingingCalls);
+ pw.decreaseIndent();
+
+ pw.println("Holding calls:");
+ pw.increaseIndent();
+ dumpCallsInCollection(pw, mHoldingCalls);
+ pw.decreaseIndent();
+
+ pw.println("Foreground call:");
+ pw.println(mForegroundCall);
}
-}
+
+ @VisibleForTesting
+ public void setIsTonePlaying(boolean isTonePlaying) {
+ mIsTonePlaying = isTonePlaying;
+ mCallAudioModeStateMachine.sendMessage(
+ isTonePlaying ? CallAudioModeStateMachine.TONE_STARTED_PLAYING
+ : CallAudioModeStateMachine.TONE_STOPPED_PLAYING,
+ makeArgsForModeStateMachine());
+ }
+
+ private void onCallLeavingState(Call call, int state) {
+ switch (state) {
+ case CallState.ACTIVE:
+ onCallLeavingActiveOrDialing();
+ break;
+ case CallState.RINGING:
+ onCallLeavingRinging();
+ break;
+ case CallState.ON_HOLD:
+ onCallLeavingHold();
+ break;
+ case CallState.DIALING:
+ stopRingbackForCall(call);
+ onCallLeavingActiveOrDialing();
+ }
+ }
+
+ private void onCallEnteringState(Call call, int state) {
+ switch (state) {
+ case CallState.ACTIVE:
+ onCallEnteringActiveOrDialing();
+ break;
+ case CallState.RINGING:
+ onCallEnteringRinging();
+ break;
+ case CallState.ON_HOLD:
+ onCallEnteringHold();
+ break;
+ case CallState.DIALING:
+ onCallEnteringActiveOrDialing();
+ playRingbackForCall(call);
+ break;
+ }
+ }
+
+ private void onCallLeavingActiveOrDialing() {
+ if (mActiveOrDialingCalls.size() == 0) {
+ mCallAudioModeStateMachine.sendMessage(
+ CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ private void onCallLeavingRinging() {
+ if (mRingingCalls.size() == 0) {
+ mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NO_MORE_RINGING_CALLS,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ private void onCallLeavingHold() {
+ if (mHoldingCalls.size() == 0) {
+ mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ private void onCallEnteringActiveOrDialing() {
+ if (mActiveOrDialingCalls.size() == 1) {
+ mCallAudioModeStateMachine.sendMessage(
+ CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ private void onCallEnteringRinging() {
+ if (mRingingCalls.size() == 1) {
+ mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NEW_RINGING_CALL,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ private void onCallEnteringHold() {
+ if (mHoldingCalls.size() == 1) {
+ mCallAudioModeStateMachine.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL,
+ makeArgsForModeStateMachine());
+ }
+ }
+
+ private void updateForegroundCall() {
+ Call oldForegroundCall = mForegroundCall;
+ if (mActiveOrDialingCalls.size() > 0) {
+ mForegroundCall = mActiveOrDialingCalls.iterator().next();
+ } else if (mRingingCalls.size() > 0) {
+ mForegroundCall = mRingingCalls.iterator().next();
+ } else if (mHoldingCalls.size() > 0) {
+ mForegroundCall = mHoldingCalls.iterator().next();
+ } else {
+ mForegroundCall = null;
+ }
+
+ if (mForegroundCall != oldForegroundCall) {
+ mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+ }
+ }
+
+ @NonNull
+ private CallAudioModeStateMachine.MessageArgs makeArgsForModeStateMachine() {
+ return new CallAudioModeStateMachine.MessageArgs(
+ mActiveOrDialingCalls.size() > 0,
+ mRingingCalls.size() > 0,
+ mHoldingCalls.size() > 0,
+ mIsTonePlaying,
+ mForegroundCall != null && mForegroundCall.getIsVoipAudioMode(),
+ Log.createSubsession());
+ }
+
+ private void playToneForDisconnectedCall(Call call) {
+ if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
+ Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
+ " and there is another call.");
+ return;
+ }
+
+ if (call.getDisconnectCause() != null) {
+ int toneToPlay = InCallTonePlayer.TONE_INVALID;
+
+ Log.v(this, "Disconnect cause: %s.", call.getDisconnectCause());
+
+ switch(call.getDisconnectCause().getTone()) {
+ case ToneGenerator.TONE_SUP_BUSY:
+ toneToPlay = InCallTonePlayer.TONE_BUSY;
+ break;
+ case ToneGenerator.TONE_SUP_CONGESTION:
+ toneToPlay = InCallTonePlayer.TONE_CONGESTION;
+ break;
+ case ToneGenerator.TONE_CDMA_REORDER:
+ toneToPlay = InCallTonePlayer.TONE_REORDER;
+ break;
+ case ToneGenerator.TONE_CDMA_ABBR_INTERCEPT:
+ toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
+ break;
+ case ToneGenerator.TONE_CDMA_CALLDROP_LITE:
+ toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
+ break;
+ case ToneGenerator.TONE_SUP_ERROR:
+ toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
+ break;
+ case ToneGenerator.TONE_PROP_PROMPT:
+ toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+ break;
+ }
+
+ Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
+
+ if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
+ mPlayerFactory.createPlayer(toneToPlay).startTone();
+ }
+ }
+ }
+
+ private void playRingbackForCall(Call call) {
+ if (call == mForegroundCall && call.isRingbackRequested()) {
+ mRingbackPlayer.startRingbackForCall(call);
+ }
+ }
+
+ private void stopRingbackForCall(Call call) {
+ mRingbackPlayer.stopRingbackForCall(call);
+ }
+
+ private void dumpCallsInCollection(IndentingPrintWriter pw, Collection<Call> calls) {
+ for (Call call : calls) {
+ if (call != null) pw.println(call.getId());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
new file mode 100644
index 0000000..8a1aef8
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.media.AudioManager;
+import android.os.Message;
+import android.util.SparseArray;
+
+import com.android.internal.util.IState;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+public class CallAudioModeStateMachine extends StateMachine {
+ public static class MessageArgs {
+ public boolean hasActiveOrDialingCalls;
+ public boolean hasRingingCalls;
+ public boolean hasHoldingCalls;
+ public boolean isTonePlaying;
+ public boolean foregroundCallIsVoip;
+ public Session session;
+
+ public MessageArgs(boolean hasActiveOrDialingCalls, boolean hasRingingCalls,
+ boolean hasHoldingCalls, boolean isTonePlaying, boolean foregroundCallIsVoip,
+ Session session) {
+ this.hasActiveOrDialingCalls = hasActiveOrDialingCalls;
+ this.hasRingingCalls = hasRingingCalls;
+ this.hasHoldingCalls = hasHoldingCalls;
+ this.isTonePlaying = isTonePlaying;
+ this.foregroundCallIsVoip = foregroundCallIsVoip;
+ this.session = session;
+ }
+
+ @Override
+ public String toString() {
+ return "MessageArgs{" +
+ "hasActiveCalls=" + hasActiveOrDialingCalls +
+ ", hasRingingCalls=" + hasRingingCalls +
+ ", hasHoldingCalls=" + hasHoldingCalls +
+ ", isTonePlaying=" + isTonePlaying +
+ ", foregroundCallIsVoip=" + foregroundCallIsVoip +
+ ", session=" + session +
+ '}';
+ }
+ }
+
+ public static final int INITIALIZE = 1;
+ // These ENTER_*_FOCUS commands are for testing.
+ public static final int ENTER_CALL_FOCUS_FOR_TESTING = 2;
+ public static final int ENTER_COMMS_FOCUS_FOR_TESTING = 3;
+ 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 ABANDON_FOCUS_FOR_TESTING = 6;
+
+ public static final int NO_MORE_ACTIVE_OR_DIALING_CALLS = 1001;
+ public static final int NO_MORE_RINGING_CALLS = 1002;
+ public static final int NO_MORE_HOLDING_CALLS = 1003;
+
+ public static final int NEW_ACTIVE_OR_DIALING_CALL = 2001;
+ public static final int NEW_RINGING_CALL = 2002;
+ public static final int NEW_HOLDING_CALL = 2003;
+ public static final int MT_AUDIO_SPEEDUP_FOR_RINGING_CALL = 2004;
+
+ public static final int TONE_STARTED_PLAYING = 3001;
+ public static final int TONE_STOPPED_PLAYING = 3002;
+
+ public static final int FOREGROUND_VOIP_MODE_CHANGE = 4001;
+
+ public static final int RUN_RUNNABLE = 9001;
+
+ private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
+ put(ENTER_CALL_FOCUS_FOR_TESTING, "ENTER_CALL_FOCUS_FOR_TESTING");
+ put(ENTER_COMMS_FOCUS_FOR_TESTING, "ENTER_COMMS_FOCUS_FOR_TESTING");
+ put(ENTER_RING_FOCUS_FOR_TESTING, "ENTER_RING_FOCUS_FOR_TESTING");
+ put(ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING, "ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING");
+ put(ABANDON_FOCUS_FOR_TESTING, "ABANDON_FOCUS_FOR_TESTING");
+ put(NO_MORE_ACTIVE_OR_DIALING_CALLS, "NO_MORE_ACTIVE_OR_DIALING_CALLS");
+ put(NO_MORE_RINGING_CALLS, "NO_MORE_RINGING_CALLS");
+ put(NO_MORE_HOLDING_CALLS, "NO_MORE_HOLDING_CALLS");
+ put(NEW_ACTIVE_OR_DIALING_CALL, "NEW_ACTIVE_OR_DIALING_CALL");
+ put(NEW_RINGING_CALL, "NEW_RINGING_CALL");
+ put(NEW_HOLDING_CALL, "NEW_HOLDING_CALL");
+ put(MT_AUDIO_SPEEDUP_FOR_RINGING_CALL, "MT_AUDIO_SPEEDUP_FOR_RINGING_CALL");
+ put(TONE_STARTED_PLAYING, "TONE_STARTED_PLAYING");
+ put(TONE_STOPPED_PLAYING, "TONE_STOPPED_PLAYING");
+ put(FOREGROUND_VOIP_MODE_CHANGE, "FOREGROUND_VOIP_MODE_CHANGE");
+
+ put(RUN_RUNNABLE, "RUN_RUNNABLE");
+ }};
+
+ public static final String TONE_HOLD_STATE_NAME = OtherFocusState.class.getSimpleName();
+ public static final String UNFOCUSED_STATE_NAME = UnfocusedState.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 COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName();
+
+ private class BaseState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case ENTER_CALL_FOCUS_FOR_TESTING:
+ transitionTo(mSimCallFocusState);
+ return HANDLED;
+ case ENTER_COMMS_FOCUS_FOR_TESTING:
+ transitionTo(mVoipCallFocusState);
+ return HANDLED;
+ case ENTER_RING_FOCUS_FOR_TESTING:
+ transitionTo(mRingingFocusState);
+ return HANDLED;
+ case ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING:
+ transitionTo(mOtherFocusState);
+ return HANDLED;
+ case ABANDON_FOCUS_FOR_TESTING:
+ transitionTo(mUnfocusedState);
+ return HANDLED;
+ case INITIALIZE:
+ mIsInitialized = true;
+ return HANDLED;
+ case RUN_RUNNABLE:
+ java.lang.Runnable r = (java.lang.Runnable) msg.obj;
+ r.run();
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private class UnfocusedState extends BaseState {
+ @Override
+ public void enter() {
+ if (mIsInitialized) {
+ Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED");
+ mAudioManager.abandonAudioFocusForCall();
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+
+ mMostRecentMode = AudioManager.MODE_NORMAL;
+ mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS);
+ }
+ }
+
+ @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:
+ // Do nothing.
+ return HANDLED;
+ case NO_MORE_RINGING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NO_MORE_HOLDING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NEW_ACTIVE_OR_DIALING_CALL:
+ transitionTo(args.foregroundCallIsVoip
+ ? mVoipCallFocusState : mSimCallFocusState);
+ return HANDLED;
+ case NEW_RINGING_CALL:
+ transitionTo(mRingingFocusState);
+ return HANDLED;
+ case NEW_HOLDING_CALL:
+ // This really shouldn't happen, but transition to the focused state anyway.
+ Log.w(LOG_TAG, "Call was surprisingly put into hold from an unknown state." +
+ " Args are: \n" + args.toString());
+ transitionTo(mOtherFocusState);
+ 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"
+ + args.toString());
+ return HANDLED;
+ default:
+ // The forced focus switch commands are handled by BaseState.
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private class RingingFocusState extends BaseState {
+ @Override
+ public void enter() {
+ Log.i(LOG_TAG, "Audio focus entering RINGING state");
+ mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ if (mMostRecentMode == AudioManager.MODE_IN_CALL) {
+ // Preserving behavior from the old CallAudioManager.
+ Log.i(LOG_TAG, "Transition from IN_CALL -> RINGTONE."
+ + " Resetting to NORMAL first.");
+ mAudioManager.setMode(AudioManager.MODE_NORMAL);
+ }
+ mAudioManager.setMode(AudioManager.MODE_RINGTONE);
+
+ mCallAudioManager.stopCallWaiting();
+ mCallAudioManager.startRinging();
+ mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+ }
+
+ @Override
+ public void exit() {
+ // Audio mode and audio stream will be set by the next state.
+ mCallAudioManager.stopRinging();
+ }
+
+ @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:
+ // Do nothing. Loss of an active call should not impact ringer.
+ return HANDLED;
+ case NO_MORE_HOLDING_CALLS:
+ // Do nothing and keep ringing.
+ return HANDLED;
+ case NO_MORE_RINGING_CALLS:
+ // If there are active or holding calls, switch to the appropriate focus.
+ // Otherwise abandon focus.
+ if (args.hasActiveOrDialingCalls) {
+ if (args.foregroundCallIsVoip) {
+ transitionTo(mVoipCallFocusState);
+ } else {
+ transitionTo(mSimCallFocusState);
+ }
+ } else if (args.hasHoldingCalls || args.isTonePlaying) {
+ transitionTo(mOtherFocusState);
+ } else {
+ transitionTo(mUnfocusedState);
+ }
+ return HANDLED;
+ case NEW_ACTIVE_OR_DIALING_CALL:
+ // If a call becomes active suddenly, give it priority over ringing.
+ transitionTo(args.foregroundCallIsVoip
+ ? mVoipCallFocusState : mSimCallFocusState);
+ return HANDLED;
+ case NEW_RINGING_CALL:
+ Log.w(LOG_TAG, "Unexpected behavior! New ringing call appeared while in " +
+ "ringing state.");
+ return HANDLED;
+ case NEW_HOLDING_CALL:
+ // This really shouldn't happen, but transition to the focused state anyway.
+ Log.w(LOG_TAG, "Call was surprisingly put into hold while ringing." +
+ " Args are: " + args.toString());
+ transitionTo(mOtherFocusState);
+ return HANDLED;
+ case MT_AUDIO_SPEEDUP_FOR_RINGING_CALL:
+ // This happens when an IMS call is answered by the in-call UI. Special case
+ // that we have to deal with for some reason.
+
+ // VOIP calls should never invoke this mechanism, so transition directly to
+ // the sim call focus state.
+ transitionTo(mSimCallFocusState);
+ return HANDLED;
+ default:
+ // The forced focus switch commands are handled by BaseState.
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private class SimCallFocusState extends BaseState {
+ @Override
+ public void enter() {
+ Log.i(LOG_TAG, "Audio focus entering SIM CALL state");
+ mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ mAudioManager.setMode(AudioManager.MODE_IN_CALL);
+ mMostRecentMode = AudioManager.MODE_IN_CALL;
+ mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+ }
+
+ @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(destinationStateAfterNoMoreActiveCalls(args));
+ return HANDLED;
+ case NO_MORE_RINGING_CALLS:
+ // Don't transition state, but stop any call-waiting tones that may have been
+ // playing.
+ if (args.isTonePlaying) {
+ mCallAudioManager.stopCallWaiting();
+ }
+ return HANDLED;
+ case NO_MORE_HOLDING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NEW_ACTIVE_OR_DIALING_CALL:
+ // Do nothing. Already active.
+ return HANDLED;
+ case NEW_RINGING_CALL:
+ // Don't make a call ring over an active call, but do play a call waiting tone.
+ mCallAudioManager.startCallWaiting();
+ return HANDLED;
+ case NEW_HOLDING_CALL:
+ // Don't do anything now. Putting an active call on hold will be handled when
+ // NO_MORE_ACTIVE_CALLS is processed.
+ return HANDLED;
+ case FOREGROUND_VOIP_MODE_CHANGE:
+ if (args.foregroundCallIsVoip) {
+ transitionTo(mVoipCallFocusState);
+ }
+ return HANDLED;
+ default:
+ // The forced focus switch commands are handled by BaseState.
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private class VoipCallFocusState extends BaseState {
+ @Override
+ public void enter() {
+ Log.i(LOG_TAG, "Audio focus entering VOIP CALL state");
+ mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+ mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
+ mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+ }
+
+ @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(destinationStateAfterNoMoreActiveCalls(args));
+ return HANDLED;
+ case NO_MORE_RINGING_CALLS:
+ // Don't transition state, but stop any call-waiting tones that may have been
+ // playing.
+ if (args.isTonePlaying) {
+ mCallAudioManager.stopCallWaiting();
+ }
+ return HANDLED;
+ case NO_MORE_HOLDING_CALLS:
+ // Do nothing.
+ return HANDLED;
+ case NEW_ACTIVE_OR_DIALING_CALL:
+ // Do nothing. Already active.
+ return HANDLED;
+ case NEW_RINGING_CALL:
+ // Don't make a call ring over an active call, but do play a call waiting tone.
+ mCallAudioManager.startCallWaiting();
+ return HANDLED;
+ case NEW_HOLDING_CALL:
+ // Don't do anything now. Putting an active call on hold will be handled when
+ // NO_MORE_ACTIVE_CALLS is processed.
+ return HANDLED;
+ case FOREGROUND_VOIP_MODE_CHANGE:
+ if (!args.foregroundCallIsVoip) {
+ transitionTo(mSimCallFocusState);
+ }
+ return HANDLED;
+ default:
+ // The forced focus switch commands are handled by BaseState.
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ /**
+ * This class is used for calls on hold and end-of-call tones.
+ */
+ private class OtherFocusState extends BaseState {
+ @Override
+ public void enter() {
+ Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state");
+ mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ mAudioManager.setMode(mMostRecentMode);
+ mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.HAS_FOCUS);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ MessageArgs args = (MessageArgs) msg.obj;
+ switch (msg.what) {
+ case NO_MORE_HOLDING_CALLS:
+ if (args.hasActiveOrDialingCalls) {
+ transitionTo(args.foregroundCallIsVoip
+ ? mVoipCallFocusState : mSimCallFocusState);
+ } else if (args.hasRingingCalls) {
+ transitionTo(mRingingFocusState);
+ } else if (!args.isTonePlaying) {
+ transitionTo(mUnfocusedState);
+ }
+ // Do nothing if a tone is playing.
+ return HANDLED;
+ case NEW_ACTIVE_OR_DIALING_CALL:
+ transitionTo(args.foregroundCallIsVoip
+ ? mVoipCallFocusState : mSimCallFocusState);
+ return HANDLED;
+ case NEW_RINGING_CALL:
+ // Apparently this is current behavior. Should this be the case?
+ transitionTo(mRingingFocusState);
+ return HANDLED;
+ case NEW_HOLDING_CALL:
+ // Do nothing.
+ return HANDLED;
+ case NO_MORE_RINGING_CALLS:
+ // If there are no more ringing calls in this state, then stop any call-waiting
+ // tones that may be playing.
+ mCallAudioManager.stopCallWaiting();
+ return HANDLED;
+ case TONE_STOPPED_PLAYING:
+ if (!args.hasActiveOrDialingCalls && !args.hasRingingCalls
+ && !args.hasHoldingCalls && !args.isTonePlaying) {
+ transitionTo(mUnfocusedState);
+ }
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private static final String LOG_TAG = CallAudioModeStateMachine.class.getSimpleName();
+
+ private final BaseState mUnfocusedState = new UnfocusedState();
+ private final BaseState mRingingFocusState = new RingingFocusState();
+ private final BaseState mSimCallFocusState = new SimCallFocusState();
+ private final BaseState mVoipCallFocusState = new VoipCallFocusState();
+ private final BaseState mOtherFocusState = new OtherFocusState();
+
+ private final AudioManager mAudioManager;
+ private CallAudioManager mCallAudioManager;
+
+ private int mMostRecentMode;
+ private boolean mIsInitialized = false;
+
+ public CallAudioModeStateMachine(AudioManager audioManager) {
+ super(CallAudioModeStateMachine.class.getSimpleName());
+ mAudioManager = audioManager;
+ mMostRecentMode = AudioManager.MODE_NORMAL;
+
+ addState(mUnfocusedState);
+ addState(mRingingFocusState);
+ addState(mSimCallFocusState);
+ addState(mVoipCallFocusState);
+ addState(mOtherFocusState);
+ setInitialState(mUnfocusedState);
+ start();
+ sendMessage(INITIALIZE);
+ }
+
+ public void setCallAudioManager(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
+ }
+
+ public String getCurrentStateName() {
+ IState currentState = getCurrentState();
+ return currentState == null ? "no state" : currentState.getName();
+ }
+
+ @Override
+ protected void onPreHandleMessage(Message msg) {
+ if (msg.obj != null && msg.obj instanceof MessageArgs) {
+ Log.continueSession(((MessageArgs) msg.obj).session, "CAMSM.pM_" + msg.what);
+ Log.i(LOG_TAG, "Message received: %s.", MESSAGE_CODE_TO_NAME.get(msg.what));
+ } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) {
+ Log.i(LOG_TAG, "Running runnable for testing");
+ } else {
+ Log.w(LOG_TAG, "Message sent must be of type nonnull MessageArgs, but got " +
+ (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName()));
+ Log.w(LOG_TAG, "The message was of code %d = %s",
+ msg.what, MESSAGE_CODE_TO_NAME.get(msg.what));
+ }
+ }
+
+ @Override
+ protected void onPostHandleMessage(Message msg) {
+ Log.endSession();
+ }
+
+ private BaseState destinationStateAfterNoMoreActiveCalls(MessageArgs args) {
+ if (args.hasRingingCalls) {
+ return mRingingFocusState;
+ } else if (args.hasHoldingCalls || args.isTonePlaying) {
+ return mOtherFocusState;
+ } else {
+ return mUnfocusedState;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
new file mode 100644
index 0000000..45e5afa
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+/**
+ * A class that acts as a listener to things that could change call audio routing, namely
+ * bluetooth status, wired headset status, and dock status.
+ */
+public class CallAudioRoutePeripheralAdapter implements BluetoothManager.BluetoothStateListener,
+ WiredHeadsetManager.Listener, DockManager.Listener {
+ private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+ private final BluetoothManager mBluetoothManager;
+
+ public CallAudioRoutePeripheralAdapter(
+ CallAudioRouteStateMachine callAudioRouteStateMachine,
+ BluetoothManager bluetoothManager,
+ WiredHeadsetManager wiredHeadsetManager,
+ DockManager dockManager) {
+ mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+ mBluetoothManager = bluetoothManager;
+
+ mBluetoothManager.setBluetoothStateListener(this);
+ wiredHeadsetManager.addListener(this);
+ dockManager.addListener(this);
+ }
+
+ @Override
+ public void onBluetoothStateChange(BluetoothManager bluetoothManager) {
+ if (bluetoothManager.isBluetoothAvailable()) {
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH);
+ } else {
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH);
+ }
+ }
+
+ public boolean isBluetoothAudioOn() {
+ return mBluetoothManager.isBluetoothAudioConnected();
+ }
+
+ /**
+ * Updates the audio route when the headset plugged in state changes. For example, if audio is
+ * being routed over speakerphone and a headset is plugged in then switch to wired headset.
+ */
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ if (!oldIsPluggedIn && newIsPluggedIn) {
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+ } else if (oldIsPluggedIn && !newIsPluggedIn){
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ }
+ }
+
+ @Override
+ public void onDockChanged(boolean isDocked) {
+ mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+ isDocked ? CallAudioRouteStateMachine.CONNECT_DOCK
+ : CallAudioRouteStateMachine.DISCONNECT_DOCK
+ );
+ }
+}
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
new file mode 100644
index 0000000..7bb213a
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -0,0 +1,1217 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.ActivityManagerNative;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.os.Binder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.telecom.CallAudioState;
+import android.util.SparseArray;
+
+import com.android.internal.util.IState;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.util.HashMap;
+
+/**
+ * This class describes the available routes of a call as a state machine.
+ * Transitions are caused solely by the commands sent as messages. Possible values for msg.what
+ * are defined as event constants in this file.
+ *
+ * The eight states are all instances of the abstract base class, {@link AudioState}. Each state
+ * is a combination of one of the four audio routes (earpiece, wired headset, bluetooth, and
+ * speakerphone) and audio focus status (active or quiescent).
+ *
+ * Messages are processed first by the processMessage method in the base class, AudioState.
+ * Any messages not completely handled by AudioState are further processed by the same method in
+ * the route-specific abstract classes: {@link EarpieceRoute}, {@link HeadsetRoute},
+ * {@link BluetoothRoute}, and {@link SpeakerRoute}. Finally, messages that are not handled at
+ * this level are then processed by the classes corresponding to the state instances themselves.
+ *
+ * There are several variables carrying additional state. These include:
+ * mAvailableRoutes: A bitmask describing which audio routes are available
+ * mWasOnSpeaker: A boolean indicating whether we should switch to speakerphone after disconnecting
+ * from a wired headset
+ * mIsMuted: a boolean indicating whether the audio is muted
+ */
+public class CallAudioRouteStateMachine extends StateMachine {
+ /** Direct the audio stream through the device's earpiece. */
+ public static final int ROUTE_EARPIECE = CallAudioState.ROUTE_EARPIECE;
+
+ /** Direct the audio stream through Bluetooth. */
+ public static final int ROUTE_BLUETOOTH = CallAudioState.ROUTE_BLUETOOTH;
+
+ /** Direct the audio stream through a wired headset. */
+ public static final int ROUTE_WIRED_HEADSET = CallAudioState.ROUTE_WIRED_HEADSET;
+
+ /** Direct the audio stream through the device's speakerphone. */
+ public static final int ROUTE_SPEAKER = CallAudioState.ROUTE_SPEAKER;
+
+ /** Valid values for msg.what */
+ public static final int CONNECT_WIRED_HEADSET = 1;
+ public static final int DISCONNECT_WIRED_HEADSET = 2;
+ public static final int CONNECT_BLUETOOTH = 3;
+ public static final int DISCONNECT_BLUETOOTH = 4;
+ public static final int CONNECT_DOCK = 5;
+ public static final int DISCONNECT_DOCK = 6;
+
+ public static final int SWITCH_EARPIECE = 1001;
+ public static final int SWITCH_BLUETOOTH = 1002;
+ public static final int SWITCH_HEADSET = 1003;
+ public static final int SWITCH_SPEAKER = 1004;
+ // Wired headset, earpiece, or speakerphone, in that order of precedence.
+ public static final int SWITCH_BASELINE_ROUTE = 1005;
+
+ public static final int REINITIALIZE = 2001;
+
+ public static final int MUTE_ON = 3001;
+ public static final int MUTE_OFF = 3002;
+ public static final int TOGGLE_MUTE = 3003;
+
+ public static final int SWITCH_FOCUS = 4001;
+
+ // Used in testing to execute verifications. Not compatible with subsessions.
+ public static final int RUN_RUNNABLE = 9001;
+
+ /** Valid values for mAudioFocusType */
+ public static final int NO_FOCUS = 1;
+ public static final int HAS_FOCUS = 2;
+
+ private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
+ put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
+ put(DISCONNECT_WIRED_HEADSET, "DISCONNECT_WIRED_HEADSET");
+ put(CONNECT_BLUETOOTH, "CONNECT_BLUETOOTH");
+ put(DISCONNECT_BLUETOOTH, "DISCONNECT_BLUETOOTH");
+ put(CONNECT_DOCK, "CONNECT_DOCK");
+ put(DISCONNECT_DOCK, "DISCONNECT_DOCK");
+
+ put(SWITCH_EARPIECE, "SWITCH_EARPIECE");
+ put(SWITCH_BLUETOOTH, "SWITCH_BLUETOOTH");
+ put(SWITCH_HEADSET, "SWITCH_HEADSET");
+ put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
+ put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
+
+ put(REINITIALIZE, "REINITIALIZE");
+
+ put(MUTE_ON, "MUTE_ON");
+ put(MUTE_OFF, "MUTE_OFF");
+ put(TOGGLE_MUTE, "TOGGLE_MUTE");
+
+ put(SWITCH_FOCUS, "SWITCH_FOCUS");
+
+ put(RUN_RUNNABLE, "RUN_RUNNABLE");
+ }};
+
+ private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
+ private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
+ private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
+ private static final String ACTIVE_HEADSET_ROUTE_NAME = "ActiveHeadsetRoute";
+ private static final String QUIESCENT_EARPIECE_ROUTE_NAME = "QuiescentEarpieceRoute";
+ private static final String QUIESCENT_BLUETOOTH_ROUTE_NAME = "QuiescentBluetoothRoute";
+ private static final String QUIESCENT_SPEAKER_ROUTE_NAME = "QuiescentSpeakerRoute";
+ private static final String QUIESCENT_HEADSET_ROUTE_NAME = "QuiescentHeadsetRoute";
+
+ public static final String NAME = CallAudioRouteStateMachine.class.getName();
+
+ @Override
+ protected void onPreHandleMessage(Message msg) {
+ if (msg.obj != null && msg.obj instanceof Session) {
+ String messageCodeName = MESSAGE_CODE_TO_NAME.get(msg.what, "unknown");
+ Log.continueSession((Session) msg.obj, "CARSM.pM_" + messageCodeName);
+ Log.i(this, "Message received: %s=%d", messageCodeName, msg.what);
+ }
+ }
+
+ @Override
+ protected void onPostHandleMessage(Message msg) {
+ Log.endSession();
+ }
+
+ abstract class AudioState extends State {
+ @Override
+ public void enter() {
+ super.enter();
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Entering state " + getName());
+ }
+
+ @Override
+ public void exit() {
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Leaving state " + getName());
+ super.exit();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Wired headset connected");
+ mAvailableRoutes &= ~ROUTE_EARPIECE;
+ mAvailableRoutes |= ROUTE_WIRED_HEADSET;
+ return NOT_HANDLED;
+ case CONNECT_BLUETOOTH:
+ // This case is here because the bluetooth manager sends out a lot of spurious
+ // state changes, and no layers above this one can tell which are actual changes
+ // in connection/disconnection status. This filters it out.
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ return HANDLED; // Do nothing if we already have bluetooth as enabled.
+ } else {
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Bluetooth connected");
+ mAvailableRoutes |= ROUTE_BLUETOOTH;
+ return NOT_HANDLED;
+ }
+ case DISCONNECT_WIRED_HEADSET:
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Wired headset disconnected");
+ mAvailableRoutes &= ~ROUTE_WIRED_HEADSET;
+ if (mDoesDeviceSupportEarpieceRoute) {
+ mAvailableRoutes |= ROUTE_EARPIECE;
+ }
+ return NOT_HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) == 0) {
+ return HANDLED;
+ } else {
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ "Bluetooth disconnected");
+ mAvailableRoutes &= ~ROUTE_BLUETOOTH;
+ return NOT_HANDLED;
+ }
+ case SWITCH_BASELINE_ROUTE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ sendInternalMessage(SWITCH_EARPIECE);
+ } else if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ sendInternalMessage(SWITCH_HEADSET);
+ } else if (!mDoesDeviceSupportEarpieceRoute) {
+ sendInternalMessage(SWITCH_SPEAKER);
+ } else {
+ Log.e(this, new IllegalStateException(),
+ "Neither headset nor earpiece are available, but there is an " +
+ "earpiece on the device. Defaulting to earpiece.");
+ sendInternalMessage(SWITCH_EARPIECE);
+ }
+ return HANDLED;
+ case REINITIALIZE:
+ CallAudioState initState = getInitialAudioState();
+ mAvailableRoutes = initState.getSupportedRouteMask();
+ mIsMuted = initState.isMuted();
+ mWasOnSpeaker = initState.getRoute() == ROUTE_SPEAKER;
+ transitionTo(mRouteCodeToQuiescentState.get(initState.getRoute()));
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+
+ // Behavior will depend on whether the state is an active one or a quiescent one.
+ abstract public void updateSystemAudioState();
+ abstract public boolean isActive();
+ }
+
+ class ActiveEarpieceRoute extends EarpieceRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_EARPIECE_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ setSpeakerphoneOn(false);
+ setBluetoothOn(false);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
+ mAvailableRoutes);
+ setSystemAudioState(newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ setSystemAudioState(mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ // Nothing to do here
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mActiveHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mActiveSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentEarpieceRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentEarpieceRoute extends EarpieceRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_EARPIECE_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ // Nothing to do here
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mQuiescentBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mQuiescentHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mQuiescentSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveEarpieceRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class EarpieceRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ sendInternalMessage(SWITCH_HEADSET);
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BLUETOOTH);
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ Log.e(this, new IllegalStateException(),
+ "Wired headset should not go from connected to not when on " +
+ "earpiece");
+ updateSystemAudioState();
+ return HANDLED;
+ case CONNECT_DOCK:
+ sendInternalMessage(SWITCH_SPEAKER);
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class ActiveHeadsetRoute extends HeadsetRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_HEADSET_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ setSpeakerphoneOn(false);
+ setBluetoothOn(false);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
+ mAvailableRoutes);
+ setSystemAudioState(newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ setSystemAudioState(mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mActiveEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mActiveSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentHeadsetRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentHeadsetRoute extends HeadsetRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_HEADSET_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mQuiescentEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mQuiescentBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mQuiescentSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveHeadsetRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class HeadsetRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ Log.e(this, new IllegalStateException(),
+ "Wired headset should already be connected.");
+ mAvailableRoutes |= ROUTE_WIRED_HEADSET;
+ updateSystemAudioState();
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BLUETOOTH);
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ if (mWasOnSpeaker) {
+ sendInternalMessage(SWITCH_SPEAKER);
+ } else {
+ sendInternalMessage(SWITCH_BASELINE_ROUTE);
+ }
+ return HANDLED;
+ case CONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class ActiveBluetoothRoute extends BluetoothRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_BLUETOOTH_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ setSpeakerphoneOn(false);
+ setBluetoothOn(true);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
+ mAvailableRoutes);
+ setSystemAudioState(newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ setSystemAudioState(mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mActiveEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mActiveHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mActiveSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentBluetoothRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentBluetoothRoute extends BluetoothRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_BLUETOOTH_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mQuiescentEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mQuiescentHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ transitionTo(mQuiescentSpeakerRoute);
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveBluetoothRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class BluetoothRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ sendInternalMessage(SWITCH_HEADSET);
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ // We can't tell when a change in bluetooth state corresponds to an
+ // actual connection or disconnection, so we'll just ignore it if we're already
+ // in the bluetooth route.
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BASELINE_ROUTE);
+ mWasOnSpeaker = false;
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case CONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class ActiveSpeakerRoute extends SpeakerRoute {
+ @Override
+ public String getName() {
+ return ACTIVE_SPEAKER_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ mWasOnSpeaker = true;
+ setSpeakerphoneOn(true);
+ setBluetoothOn(false);
+ CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
+ mAvailableRoutes);
+ setSystemAudioState(newState);
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ setSystemAudioState(mCurrentCallAudioState);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch(msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mActiveEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mActiveBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mActiveHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == NO_FOCUS) {
+ transitionTo(mQuiescentSpeakerRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class QuiescentSpeakerRoute extends SpeakerRoute {
+ @Override
+ public String getName() {
+ return QUIESCENT_SPEAKER_ROUTE_NAME;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ // Omit setting mWasOnSpeaker to true here, since this does not reflect a call
+ // actually being on speakerphone.
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public void updateSystemAudioState() {
+ updateInternalCallAudioState();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch(msg.what) {
+ case SWITCH_EARPIECE:
+ if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+ transitionTo(mQuiescentEarpieceRoute);
+ } else {
+ Log.w(this, "Ignoring switch to earpiece command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_BLUETOOTH:
+ if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+ transitionTo(mQuiescentBluetoothRoute);
+ } else {
+ Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_HEADSET:
+ if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+ transitionTo(mQuiescentHeadsetRoute);
+ } else {
+ Log.w(this, "Ignoring switch to headset command. Not available.");
+ }
+ return HANDLED;
+ case SWITCH_SPEAKER:
+ // Nothing to do
+ return HANDLED;
+ case SWITCH_FOCUS:
+ if (msg.arg1 == HAS_FOCUS) {
+ transitionTo(mActiveSpeakerRoute);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ abstract class SpeakerRoute extends AudioState {
+ @Override
+ public boolean processMessage(Message msg) {
+ if (super.processMessage(msg) == HANDLED) {
+ return HANDLED;
+ }
+ switch (msg.what) {
+ case CONNECT_WIRED_HEADSET:
+ sendInternalMessage(SWITCH_HEADSET);
+ return HANDLED;
+ case CONNECT_BLUETOOTH:
+ sendInternalMessage(SWITCH_BLUETOOTH);
+ return HANDLED;
+ case DISCONNECT_BLUETOOTH:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case DISCONNECT_WIRED_HEADSET:
+ updateSystemAudioState();
+ // No change in audio route required
+ return HANDLED;
+ case CONNECT_DOCK:
+ // Nothing to do here
+ return HANDLED;
+ case DISCONNECT_DOCK:
+ sendInternalMessage(SWITCH_BASELINE_ROUTE);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ private final ActiveEarpieceRoute mActiveEarpieceRoute = new ActiveEarpieceRoute();
+ private final ActiveHeadsetRoute mActiveHeadsetRoute = new ActiveHeadsetRoute();
+ private final ActiveBluetoothRoute mActiveBluetoothRoute = new ActiveBluetoothRoute();
+ private final ActiveSpeakerRoute mActiveSpeakerRoute = new ActiveSpeakerRoute();
+ private final QuiescentEarpieceRoute mQuiescentEarpieceRoute = new QuiescentEarpieceRoute();
+ private final QuiescentHeadsetRoute mQuiescentHeadsetRoute = new QuiescentHeadsetRoute();
+ private final QuiescentBluetoothRoute mQuiescentBluetoothRoute = new QuiescentBluetoothRoute();
+ private final QuiescentSpeakerRoute mQuiescentSpeakerRoute = new QuiescentSpeakerRoute();
+
+ /**
+ * A few pieces of hidden state. Used to avoid exponential explosion of number of explicit
+ * states
+ */
+ private int mAvailableRoutes;
+ private boolean mWasOnSpeaker;
+ private boolean mIsMuted;
+
+ private final Context mContext;
+ private final CallsManager mCallsManager;
+ private final AudioManager mAudioManager;
+ private final BluetoothManager mBluetoothManager;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+ private final StatusBarNotifier mStatusBarNotifier;
+ private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+ private final boolean mDoesDeviceSupportEarpieceRoute;
+
+ private HashMap<String, Integer> mStateNameToRouteCode;
+ private HashMap<Integer, AudioState> mRouteCodeToQuiescentState;
+
+ // CallAudioState is used as an interface to communicate with many other system components.
+ // No internal state transitions should depend on this variable.
+ private CallAudioState mCurrentCallAudioState;
+ private CallAudioState mLastKnownCallAudioState;
+
+ public CallAudioRouteStateMachine(
+ Context context,
+ CallsManager callsManager,
+ BluetoothManager bluetoothManager,
+ WiredHeadsetManager wiredHeadsetManager,
+ StatusBarNotifier statusBarNotifier,
+ CallAudioManager.AudioServiceFactory audioServiceFactory,
+ boolean doesDeviceSupportEarpieceRoute) {
+ super(NAME);
+ addState(mActiveEarpieceRoute);
+ addState(mActiveHeadsetRoute);
+ addState(mActiveBluetoothRoute);
+ addState(mActiveSpeakerRoute);
+ addState(mQuiescentEarpieceRoute);
+ addState(mQuiescentHeadsetRoute);
+ addState(mQuiescentBluetoothRoute);
+ addState(mQuiescentSpeakerRoute);
+
+ mContext = context;
+ mCallsManager = callsManager;
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mBluetoothManager = bluetoothManager;
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mStatusBarNotifier = statusBarNotifier;
+ mAudioServiceFactory = audioServiceFactory;
+ mDoesDeviceSupportEarpieceRoute = doesDeviceSupportEarpieceRoute;
+
+ mStateNameToRouteCode = new HashMap<>(8);
+ mStateNameToRouteCode.put(mQuiescentEarpieceRoute.getName(), ROUTE_EARPIECE);
+ mStateNameToRouteCode.put(mQuiescentBluetoothRoute.getName(), ROUTE_BLUETOOTH);
+ mStateNameToRouteCode.put(mQuiescentHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
+ mStateNameToRouteCode.put(mQuiescentSpeakerRoute.getName(), ROUTE_SPEAKER);
+ mStateNameToRouteCode.put(mActiveEarpieceRoute.getName(), ROUTE_EARPIECE);
+ mStateNameToRouteCode.put(mActiveBluetoothRoute.getName(), ROUTE_BLUETOOTH);
+ mStateNameToRouteCode.put(mActiveHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
+ mStateNameToRouteCode.put(mActiveSpeakerRoute.getName(), ROUTE_SPEAKER);
+
+ 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);
+ }
+
+ /**
+ * Initializes the state machine with info on initial audio route, supported audio routes,
+ * and mute status.
+ */
+ public void initialize() {
+ CallAudioState initState = getInitialAudioState();
+ initialize(initState);
+ }
+
+ public void initialize(CallAudioState initState) {
+ mCurrentCallAudioState = initState;
+ mLastKnownCallAudioState = initState;
+ mAvailableRoutes = initState.getSupportedRouteMask();
+ mIsMuted = initState.isMuted();
+ mWasOnSpeaker = initState.getRoute() == ROUTE_SPEAKER;
+
+ mStatusBarNotifier.notifyMute(initState.isMuted());
+ mStatusBarNotifier.notifySpeakerphone(initState.getRoute() == CallAudioState.ROUTE_SPEAKER);
+ setInitialState(mRouteCodeToQuiescentState.get(initState.getRoute()));
+ start();
+ }
+
+ /**
+ * Getter for the current CallAudioState object that the state machine is keeping track of.
+ * Used for compatibility purposes.
+ */
+ public CallAudioState getCurrentCallAudioState() {
+ return mCurrentCallAudioState;
+ }
+
+ public void sendMessageWithSessionInfo(int message, int arg) {
+ sendMessage(message, arg, 0, Log.createSubsession());
+ }
+
+ public void sendMessageWithSessionInfo(int message) {
+ sendMessage(message, 0, 0, Log.createSubsession());
+ }
+
+ /**
+ * This is for state-independent changes in audio route (i.e. muting or runnables)
+ * @param msg that couldn't be handled.
+ */
+ @Override
+ protected void unhandledMessage(Message msg) {
+ CallAudioState newCallAudioState;
+ switch (msg.what) {
+ case MUTE_ON:
+ setMuteOn(true);
+ newCallAudioState = new CallAudioState(mIsMuted,
+ mCurrentCallAudioState.getRoute(),
+ mAvailableRoutes);
+ setSystemAudioState(newCallAudioState);
+ updateInternalCallAudioState();
+ return;
+ case MUTE_OFF:
+ setMuteOn(false);
+ newCallAudioState = new CallAudioState(mIsMuted,
+ mCurrentCallAudioState.getRoute(),
+ mAvailableRoutes);
+ setSystemAudioState(newCallAudioState);
+ updateInternalCallAudioState();
+ return;
+ case TOGGLE_MUTE:
+ if (mIsMuted) {
+ sendInternalMessage(MUTE_OFF);
+ } else {
+ sendInternalMessage(MUTE_ON);
+ }
+ return;
+ case RUN_RUNNABLE:
+ java.lang.Runnable r = (java.lang.Runnable) msg.obj;
+ r.run();
+ return;
+ default:
+ Log.e(this, new IllegalStateException(),
+ "Unexpected message code");
+ }
+ }
+
+ public void quitStateMachine() {
+ quitNow();
+ }
+
+ private void setSpeakerphoneOn(boolean on) {
+ if (mAudioManager.isSpeakerphoneOn() != on) {
+ Log.i(this, "turning speaker phone %s", on);
+ mAudioManager.setSpeakerphoneOn(on);
+ }
+ }
+
+ private void setBluetoothOn(boolean on) {
+ if (mBluetoothManager.isBluetoothAvailable()) {
+ boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
+ if (on != isAlreadyOn) {
+ Log.i(this, "connecting bluetooth %s", on);
+ if (on) {
+ mBluetoothManager.connectBluetoothAudio();
+ } else {
+ mBluetoothManager.disconnectBluetoothAudio();
+ }
+ }
+ }
+ }
+
+ private void setMuteOn(boolean mute) {
+ mIsMuted = mute;
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
+ mute ? "on" : "off");
+ if (mute != mAudioManager.isMicrophoneMute() && isInActiveState()) {
+ IAudioService audio = mAudioServiceFactory.getAudioService();
+ Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
+ mute, audio == null);
+ if (audio != null) {
+ try {
+ // We use the audio service directly here so that we can specify
+ // the current user. Telecom runs in the system_server process which
+ // may run as a separate user from the foreground user. If we
+ // used AudioManager directly, we would change mute for the system's
+ // user and not the current foreground, which we want to avoid.
+ audio.setMicrophoneMute(
+ mute, mContext.getOpPackageName(), getCurrentUserId());
+
+ } catch (RemoteException e) {
+ Log.e(this, e, "Remote exception while toggling mute.");
+ }
+ // TODO: Check microphone state after attempting to set to ensure that
+ // our state corroborates AudioManager's state.
+ }
+ }
+ }
+
+ /**
+ * Updates the CallAudioState object from current internal state. The result is used for
+ * external communication only.
+ */
+ private void updateInternalCallAudioState() {
+ IState currentState = getCurrentState();
+ if (currentState == null) {
+ Log.e(this, new IllegalStateException(), "Current state should never be null" +
+ " when updateInternalCallAudioState is called.");
+ mCurrentCallAudioState = new CallAudioState(
+ mIsMuted, mCurrentCallAudioState.getRoute(), mAvailableRoutes);
+ return;
+ }
+ int currentRoute = mStateNameToRouteCode.get(currentState.getName());
+ mCurrentCallAudioState = new CallAudioState(mIsMuted, currentRoute, mAvailableRoutes);
+ }
+
+ private void setSystemAudioState(CallAudioState newCallAudioState) {
+ Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState,
+ newCallAudioState);
+ Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+ CallAudioState.audioRouteToString(newCallAudioState.getRoute()));
+
+ if (!newCallAudioState.equals(mLastKnownCallAudioState)) {
+ mCallsManager.onCallAudioStateChanged(mLastKnownCallAudioState, newCallAudioState);
+ updateAudioForForegroundCall(newCallAudioState);
+ mLastKnownCallAudioState = newCallAudioState;
+ }
+ }
+
+ private void updateAudioForForegroundCall(CallAudioState newCallAudioState) {
+ Call call = mCallsManager.getForegroundCall();
+ if (call != null && call.getConnectionService() != null) {
+ call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ }
+ }
+
+ private int calculateSupportedRoutes() {
+ int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+ } else if (mDoesDeviceSupportEarpieceRoute){
+ routeMask |= CallAudioState.ROUTE_EARPIECE;
+ }
+
+ if (mBluetoothManager.isBluetoothAvailable()) {
+ routeMask |= CallAudioState.ROUTE_BLUETOOTH;
+ }
+
+ return routeMask;
+ }
+
+ private void sendInternalMessage(int messageCode) {
+ // Internal messages are messages which the state machine sends to itself in the
+ // course of processing externally-sourced messages. We want to send these messages at
+ // the front of the queue in order to make actions appear atomic to the user and to
+ // prevent scenarios such as these:
+ // 1. State machine handler thread is suspended for some reason.
+ // 2. Headset gets connected (sends CONNECT_HEADSET).
+ // 3. User switches to speakerphone in the UI (sends SWITCH_SPEAKER).
+ // 4. State machine handler is un-suspended.
+ // 5. State machine handler processes the CONNECT_HEADSET message and sends
+ // SWITCH_HEADSET at end of queue.
+ // 6. State machine handler processes SWITCH_SPEAKER.
+ // 7. State machine handler processes SWITCH_HEADSET.
+ Session subsession = Log.createSubsession();
+ if(subsession != null) {
+ sendMessageAtFrontOfQueue(messageCode, subsession);
+ } else {
+ sendMessageAtFrontOfQueue(messageCode);
+ }
+ }
+
+ private CallAudioState getInitialAudioState() {
+ int supportedRouteMask = calculateSupportedRoutes();
+ final int route;
+
+ if ((supportedRouteMask & ROUTE_BLUETOOTH) != 0) {
+ route = ROUTE_BLUETOOTH;
+ } else if ((supportedRouteMask & ROUTE_WIRED_HEADSET) != 0) {
+ route = ROUTE_WIRED_HEADSET;
+ } else if ((supportedRouteMask & ROUTE_EARPIECE) != 0) {
+ route = ROUTE_EARPIECE;
+ } else {
+ route = ROUTE_SPEAKER;
+ }
+
+ return new CallAudioState(false, route, supportedRouteMask);
+ }
+
+ private int getCurrentUserId() {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ UserInfo currentUser = ActivityManagerNative.getDefault().getCurrentUser();
+ return currentUser.id;
+ } catch (RemoteException e) {
+ // Activity manager not running, nothing we can do assume user 0.
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return UserHandle.USER_OWNER;
+ }
+
+ private boolean isInActiveState() {
+ AudioState currentState = (AudioState) getCurrentState();
+ if (currentState == null) {
+ Log.w(this, "Current state is null, assuming inactive state");
+ return false;
+ }
+ return currentState.isActive();
+ }
+
+ public static boolean doesDeviceSupportEarpieceRoute() {
+ String[] characteristics = SystemProperties.get("ro.build.characteristics").split(",");
+ for (String characteristic : characteristics) {
+ if ("watch".equals(characteristic)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallIdMapper.java b/src/com/android/server/telecom/CallIdMapper.java
index 8199dfa..b097bea 100644
--- a/src/com/android/server/telecom/CallIdMapper.java
+++ b/src/com/android/server/telecom/CallIdMapper.java
@@ -18,10 +18,13 @@
import android.util.ArrayMap;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.Map;
/** Utility to map {@link Call} objects to unique IDs. IDs are generated when a call is added. */
-class CallIdMapper {
+@VisibleForTesting
+public class CallIdMapper {
/**
* A very basic bidirectional map.
*/
@@ -75,12 +78,6 @@
}
private final BiMap<String, Call> mCalls = new BiMap<>();
- private final String mCallIdPrefix;
- private static int sIdCount;
-
- CallIdMapper(String callIdPrefix) {
- mCallIdPrefix = callIdPrefix + "@";
- }
void replaceCall(Call newCall, Call callToReplace) {
// Use the old call's ID for the new call.
@@ -96,7 +93,7 @@
}
void addCall(Call call) {
- addCall(call, getNewId());
+ addCall(call, call.getId());
}
void removeCall(Call call) {
@@ -111,10 +108,10 @@
}
String getCallId(Call call) {
- if (call == null) {
+ if (call == null || mCalls.getKey(call) == null) {
return null;
}
- return mCalls.getKey(call);
+ return call.getId();
}
Call getCall(Object objId) {
@@ -122,9 +119,6 @@
if (objId instanceof String) {
callId = (String) objId;
}
- if (!isValidCallId(callId) && !isValidConferenceId(callId)) {
- return null;
- }
return mCalls.getValue(callId);
}
@@ -132,18 +126,4 @@
void clear() {
mCalls.clear();
}
-
- boolean isValidCallId(String callId) {
- // Note, no need for thread check, this method is thread safe.
- return callId != null && callId.startsWith(mCallIdPrefix);
- }
-
- boolean isValidConferenceId(String callId) {
- return callId != null;
- }
-
- String getNewId() {
- sIdCount++;
- return mCallIdPrefix + sIdCount;
- }
}
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index ea9402e..2172a81 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -8,7 +8,9 @@
import android.os.Bundle;
import android.os.Trace;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telecom.Connection;
+import android.telecom.DefaultDialerManager;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -24,6 +26,30 @@
* which interacts with the rest of Telecom, both of which run only as the primary user.
*/
public class CallIntentProcessor {
+ public interface Adapter {
+ void processOutgoingCallIntent(Context context, CallsManager callsManager,
+ Intent intent);
+ void processIncomingCallIntent(CallsManager callsManager, Intent intent);
+ void processUnknownCallIntent(CallsManager callsManager, Intent intent);
+ }
+
+ public static class AdapterImpl implements Adapter {
+ @Override
+ public void processOutgoingCallIntent(Context context, CallsManager callsManager,
+ Intent intent) {
+ CallIntentProcessor.processOutgoingCallIntent(context, callsManager, intent);
+ }
+
+ @Override
+ public void processIncomingCallIntent(CallsManager callsManager, Intent intent) {
+ CallIntentProcessor.processIncomingCallIntent(callsManager, intent);
+ }
+
+ @Override
+ public void processUnknownCallIntent(CallsManager callsManager, Intent intent) {
+ CallIntentProcessor.processUnknownCallIntent(callsManager, intent);
+ }
+ }
public static final String KEY_IS_UNKNOWN_CALL = "is_unknown_call";
public static final String KEY_IS_INCOMING_CALL = "is_incoming_call";
@@ -33,6 +59,12 @@
*/
public static final String KEY_IS_PRIVILEGED_DIALER = "is_privileged_dialer";
+ /**
+ * The user initiating the outgoing call.
+ */
+ public static final String KEY_INITIATING_USER = "initiating_user";
+
+
private final Context mContext;
private final CallsManager mCallsManager;
@@ -93,8 +125,12 @@
final boolean isPrivilegedDialer = intent.getBooleanExtra(KEY_IS_PRIVILEGED_DIALER, false);
+ fixInitiatingUserIfNecessary(context, intent);
+ UserHandle initiatingUser = intent.getParcelableExtra(KEY_INITIATING_USER);
+
// Send to CallsManager to ensure the InCallUI gets kicked off before the broadcast returns
- Call call = callsManager.startOutgoingCall(handle, phoneAccountHandle, clientExtras);
+ Call call = callsManager
+ .startOutgoingCall(handle, phoneAccountHandle, clientExtras, initiatingUser);
if (call != null) {
// Asynchronous calls should not usually be made inside a BroadcastReceiver because once
@@ -103,7 +139,8 @@
// process will be running throughout the duration of the phone call and should never
// be killed.
NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
- context, callsManager, call, intent, isPrivilegedDialer);
+ context, callsManager, call, intent, new PhoneNumberUtilsAdapterImpl(),
+ isPrivilegedDialer);
final int result = broadcaster.processIntent();
final boolean success = result == DisconnectCause.NOT_DISCONNECTED;
@@ -113,6 +150,25 @@
}
}
+ /**
+ * If the call is initiated from managed profile but there is no work dialer installed, treat
+ * the call is initiated from its parent user.
+ */
+ static void fixInitiatingUserIfNecessary(Context context, Intent intent) {
+ final UserHandle initiatingUser = intent.getParcelableExtra(KEY_INITIATING_USER);
+ if (UserUtil.isManagedProfile(context, initiatingUser)) {
+ boolean noDialerInstalled = DefaultDialerManager.getInstalledDialerApplications(context,
+ initiatingUser.getIdentifier()).size() == 0;
+ if (noDialerInstalled) {
+ final UserManager userManager = UserManager.get(context);
+ UserHandle parentUserHandle =
+ userManager.getProfileParent(
+ initiatingUser.getIdentifier()).getUserHandle();
+ intent.putExtra(KEY_INITIATING_USER, parentUserHandle);
+ }
+ }
+ }
+
static void processIncomingCallIntent(CallsManager callsManager, Intent intent) {
PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 718f2ea..bf9aa39 100755
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -20,13 +20,16 @@
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.UserHandle;
import android.provider.CallLog.Calls;
import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.VideoProfile;
import android.telephony.PhoneNumberUtils;
// TODO: Needed for move to system service: import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.CallerInfo;
/**
@@ -34,7 +37,8 @@
* caller details to the call log. All logging activity will be performed asynchronously in a
* background thread to avoid blocking on the main thread.
*/
-final class CallLogManager extends CallsManagerListenerBase {
+@VisibleForTesting
+public final class CallLogManager extends CallsManagerListenerBase {
/**
* Parameter object to hold the arguments to add a call in the call log DB.
*/
@@ -51,12 +55,14 @@
* @param durationInMillis Duration of the call (milliseconds).
* @param dataUsage Data usage in bytes, or null if not applicable.
*/
- public AddCallArgs(Context context, CallerInfo callerInfo, String number,
+ public AddCallArgs(Context context, CallerInfo callerInfo, String number, String postDialDigits,
int presentation, int callType, int features, PhoneAccountHandle accountHandle,
- long creationDate, long durationInMillis, Long dataUsage) {
+ long creationDate, long durationInMillis, Long dataUsage,
+ UserHandle initiatingUser) {
this.context = context;
this.callerInfo = callerInfo;
this.number = number;
+ this.postDialDigits = postDialDigits;
this.presentation = presentation;
this.callType = callType;
this.features = features;
@@ -64,12 +70,14 @@
this.timestamp = creationDate;
this.durationInSec = (int)(durationInMillis / 1000);
this.dataUsage = dataUsage;
+ this.initiatingUser = initiatingUser;
}
// Since the members are accessed directly, we don't use the
// mXxxx notation.
public final Context context;
public final CallerInfo callerInfo;
public final String number;
+ public final String postDialDigits;
public final int presentation;
public final int callType;
public final int features;
@@ -77,11 +85,13 @@
public final long timestamp;
public final int durationInSec;
public final Long dataUsage;
+ public final UserHandle initiatingUser;
}
private static final String TAG = CallLogManager.class.getSimpleName();
private final Context mContext;
+ private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private static final String ACTION_CALLS_TABLE_ADD_ENTRY =
"com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
private static final String PERMISSION_PROCESS_CALLLOG_INFO =
@@ -89,8 +99,9 @@
private static final String CALL_TYPE = "callType";
private static final String CALL_DURATION = "duration";
- public CallLogManager(Context context) {
+ public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar) {
mContext = context;
+ mPhoneAccountRegistrar = phoneAccountRegistrar;
}
@Override
@@ -149,9 +160,9 @@
call.getCallDataUsage();
int callFeatures = getCallFeatures(call.getVideoStateHistory());
- logCall(call.getCallerInfo(), logNumber, call.getHandlePresentation(),
- callLogType, callFeatures, accountHandle, creationTime, age, callDataUsage,
- call.isEmergencyCall());
+ logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(),
+ call.getHandlePresentation(), callLogType, callFeatures, accountHandle,
+ creationTime, age, callDataUsage, call.isEmergencyCall(), call.getInitiatingUser());
}
/**
@@ -159,6 +170,8 @@
*
* @param callerInfo Caller details.
* @param number The number the call was made to or from.
+ * @param postDialDigits The post-dial digits that were dialed after the number,
+ * if it was an outgoing call. Otherwise ''.
* @param presentation
* @param callType The type of call.
* @param features The features of the call.
@@ -170,6 +183,7 @@
private void logCall(
CallerInfo callerInfo,
String number,
+ String postDialDigits,
int presentation,
int callType,
int features,
@@ -177,7 +191,8 @@
long start,
long duration,
Long dataUsage,
- boolean isEmergency) {
+ boolean isEmergency,
+ UserHandle initiatingUser) {
// On some devices, to avoid accidental redialing of emergency numbers, we *never* log
// emergency calls to the Call Log. (This behavior is set on a per-product basis, based
@@ -194,8 +209,9 @@
Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", "
+ Log.pii(number) + "," + presentation + ", " + callType
+ ", " + start + ", " + duration);
- AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, presentation,
- callType, features, accountHandle, start, duration, dataUsage);
+ AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, postDialDigits,
+ presentation, callType, features, accountHandle, start, duration, dataUsage,
+ initiatingUser);
logCallAsync(args);
} else {
Log.d(TAG, "Not adding emergency call to call log.");
@@ -259,12 +275,9 @@
Uri[] result = new Uri[count];
for (int i = 0; i < count; i++) {
AddCallArgs c = callList[i];
-
try {
// May block.
- result[i] = Calls.addCall(c.callerInfo, c.context, c.number, c.presentation,
- c.callType, c.features, c.accountHandle, c.timestamp, c.durationInSec,
- c.dataUsage, true /* addForAllUsers */);
+ result[i] = addCall(c);
} catch (Exception e) {
// This is very rare but may happen in legitimate cases.
// E.g. If the phone is encrypted and thus write request fails, it may cause
@@ -280,6 +293,34 @@
return result;
}
+ private Uri addCall(AddCallArgs c) {
+ PhoneAccount phoneAccount = mPhoneAccountRegistrar
+ .getPhoneAccountUnchecked(c.accountHandle);
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
+ if (c.initiatingUser != null &&
+ UserUtil.isManagedProfile(mContext, c.initiatingUser)) {
+ return addCall(c, c.initiatingUser);
+ } else {
+ return addCall(c, null);
+ }
+ } else {
+ return addCall(c, c.accountHandle.getUserHandle());
+ }
+ }
+
+ /**
+ * Insert the call to a specific user or all users except managed profile.
+ * @param c context
+ * @param userToBeInserted user handle of user that the call going be inserted to. null
+ * if insert to all users except managed profile.
+ */
+ private Uri addCall(AddCallArgs c, UserHandle userToBeInserted) {
+ return Calls.addCall(c.callerInfo, c.context, c.number, c.postDialDigits,
+ c.presentation, c.callType, c.features, c.accountHandle, c.timestamp,
+ c.durationInSec, c.dataUsage, userToBeInserted == null,
+ userToBeInserted);
+ }
+
/**
* Performs a simple sanity check to make sure the call was written in the database.
* Typically there is only one result per call so it is easy to identify which one failed.
diff --git a/src/com/android/server/telecom/CallScreening.java b/src/com/android/server/telecom/CallScreening.java
new file mode 100644
index 0000000..f0ef5a6
--- /dev/null
+++ b/src/com/android/server/telecom/CallScreening.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.DefaultDialerManager;
+import android.telecom.CallScreeningService;
+import android.text.TextUtils;
+
+import com.android.internal.telecom.ICallScreeningAdapter;
+import com.android.internal.telecom.ICallScreeningService;
+
+import java.util.List;
+
+/**
+ * Binds to {@link ICallScreeningService} to allow call blocking. A single instance of this class
+ * handles a single call.
+ */
+public class CallScreening {
+ public interface Listener {
+ void onCallScreeningCompleted(
+ Call call,
+ boolean shouldAllowCall,
+ boolean shouldReject,
+ boolean shouldAddToCallLog,
+ boolean shouldShowNotification);
+ }
+
+ private final Context mContext;
+ private final Listener mListener;
+ private final TelecomSystem.SyncRoot mLock;
+ private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+ private final Handler mHandler = new Handler();
+ private Call mCall;
+ private ICallScreeningService mService;
+ private ServiceConnection mConnection;
+
+ public CallScreening(
+ Context context,
+ Listener listener,
+ TelecomSystem.SyncRoot lock,
+ PhoneAccountRegistrar phoneAccountRegistrar,
+ Call call) {
+ mContext = context;
+ mListener = listener;
+ mLock = lock;
+ mPhoneAccountRegistrar = phoneAccountRegistrar;
+ mCall = call;
+ }
+
+ public void screenCall() {
+ if (!bindService()) {
+ Log.d(this, "no service, giving up");
+ performCleanup();
+ } else {
+ mHandler.postDelayed(new Runnable("CS.sC") {
+ @Override
+ public void loggedRun() {
+ synchronized (mLock) {
+ Log.event(mCall, Log.Events.SCREENING_TIMED_OUT);
+ performCleanup();
+ }
+ }
+ }.prepare(), Timeouts.getCallScreeningTimeoutMillis(mContext.getContentResolver()));
+ }
+ }
+
+ private void performCleanup() {
+ if (mCall != null) {
+ mListener.onCallScreeningCompleted(mCall, true, false, false, false);
+ mCall = null;
+ }
+ if (mConnection != null) {
+ // We still need to call unbind even if the service disconnected.
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ mHandler.removeCallbacksAndMessages(null);
+ mService = null;
+ }
+
+ private boolean bindService() {
+ String dialerPackage = DefaultDialerManager
+ .getDefaultDialerApplication(mContext, UserHandle.USER_CURRENT);
+ if (TextUtils.isEmpty(dialerPackage)) {
+ return false;
+ }
+
+ Intent intent = new Intent(CallScreeningService.SERVICE_INTERFACE)
+ .setPackage(dialerPackage);
+ List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServices(intent, 0);
+ if (entries.isEmpty()) {
+ return false;
+ }
+
+ ResolveInfo entry = entries.get(0);
+ if (entry.serviceInfo == null) {
+ return false;
+ }
+
+ if (entry.serviceInfo.permission == null || !entry.serviceInfo.permission.equals(
+ Manifest.permission.BIND_SCREENING_SERVICE)) {
+ Log.w(this, "CallScreeningService must require BIND_SCREENING_SERVICE permission: " +
+ entry.serviceInfo.packageName);
+ return false;
+ }
+
+ ComponentName componentName =
+ new ComponentName(entry.serviceInfo.packageName, entry.serviceInfo.name);
+ Log.event(mCall, Log.Events.BIND_SCREENING, componentName);
+ intent.setComponent(componentName);
+ ServiceConnection connection = new CallScreeningServiceConnection();
+ if (mContext.bindServiceAsUser(
+ intent,
+ connection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ UserHandle.CURRENT)) {
+ Log.d(this, "bindService, found service, waiting for it to connect");
+ mConnection = connection;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void onServiceBound(ICallScreeningService service) {
+ mService = service;
+ try {
+ mService.screenCall(new CallScreeningAdapter(), ParcelableCallUtils.toParcelableCall(
+ mCall, false /* includeVideoProvider */, mPhoneAccountRegistrar));
+ } catch (RemoteException e) {
+ Log.e(this, e, "Failed to set the call screening adapter.");
+ performCleanup();
+ }
+ }
+
+ private class CallScreeningServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ Log.startSession("CSCR.oSC");
+ try {
+ synchronized (mLock) {
+ Log.event(mCall, Log.Events.SCREENING_BOUND, componentName);
+ if (mCall == null) {
+ performCleanup();
+ } else {
+ onServiceBound(ICallScreeningService.Stub.asInterface(service));
+ }
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ Log.startSession("CSCR.oSD");
+ try {
+ synchronized (mLock) {
+ performCleanup();
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ }
+
+ private class CallScreeningAdapter extends ICallScreeningAdapter.Stub {
+ @Override
+ public void allowCall(String callId) {
+ try {
+ Log.startSession("CSCR.aC");
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.d(this, "allowCall(%s)", callId);
+ if (mCall != null && mCall.getId().equals(callId)) {
+ mListener.onCallScreeningCompleted(
+ mCall,
+ true /* shouldAllowCall */,
+ false /* shouldReject */,
+ false /* shouldAddToCallLog */,
+ false /* shouldShowNotification */);
+ } else {
+ Log.w(this, "allowCall, unknown call id: %s", callId);
+ }
+ mCall = null;
+ performCleanup();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void disallowCall(
+ String callId,
+ boolean shouldReject,
+ boolean shouldAddToCallLog,
+ boolean shouldShowNotification) {
+ try {
+ Log.startSession("CSCR.dC");
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.i(this, "disallowCall(%s), shouldReject: %b, shouldAddToCallLog: %b, "
+ + "shouldShowNotification: %b", callId, shouldReject,
+ shouldAddToCallLog, shouldShowNotification);
+ if (mCall != null && mCall.getId().equals(callId)) {
+ mListener.onCallScreeningCompleted(
+ mCall,
+ false /* shouldAllowCall */,
+ shouldReject,
+ shouldAddToCallLog,
+ shouldShowNotification);
+ } else {
+ Log.w(this, "disallowCall, unknown call id: %s", callId);
+ }
+ mCall = null;
+ performCleanup();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 973e4c1..fbb35ba 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -17,13 +17,20 @@
package com.android.server.telecom;
import android.content.Context;
+import android.content.pm.UserInfo;
+import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.Process;
import android.os.SystemProperties;
+import android.os.SystemVibrator;
import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.CallLog.Calls;
+import android.provider.Settings;
import android.telecom.CallAudioState;
import android.telecom.Conference;
import android.telecom.Connection;
@@ -46,8 +53,10 @@
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -60,10 +69,12 @@
* beyond the com.android.server.telecom package boundary.
*/
@VisibleForTesting
-public class CallsManager extends Call.ListenerBase implements VideoProviderProxy.Listener {
+public class CallsManager extends Call.ListenerBase
+ implements VideoProviderProxy.Listener, CallScreening.Listener {
// TODO: Consider renaming this CallsManagerPlugin.
- interface CallsManagerListener {
+ @VisibleForTesting
+ public interface CallsManagerListener {
void onCallAdded(Call call);
void onCallRemoved(Call call);
void onCallStateChanged(Call call, int oldState, int newState);
@@ -73,7 +84,6 @@
ConnectionServiceWrapper newService);
void onIncomingCallAnswered(Call call);
void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
- void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
void onRingbackRequested(Call call, boolean ringback);
void onIsConferencedChanged(Call call);
@@ -97,6 +107,19 @@
private static final int[] LIVE_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING, CallState.ACTIVE};
+ public static final String TELECOM_CALL_ID_PREFIX = "TC@";
+
+ // Maps call technologies in PhoneConstants to those in Analytics.
+ private static final Map<Integer, Integer> sAnalyticsTechnologyMap;
+ static {
+ sAnalyticsTechnologyMap = new HashMap<>(5);
+ sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_CDMA, Analytics.CDMA_PHONE);
+ sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_GSM, Analytics.GSM_PHONE);
+ sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_IMS, Analytics.IMS_PHONE);
+ sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_SIP, Analytics.SIP_PHONE);
+ sAnalyticsTechnologyMap.put(PhoneConstants.PHONE_TYPE_THIRD_PARTY,
+ Analytics.THIRD_PARTY_PHONE);
+ }
/**
* The main call repository. Keeps an instance of all live calls. New incoming and outgoing
@@ -109,6 +132,13 @@
private final Set<Call> mCalls = Collections.newSetFromMap(
new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
+ /**
+ * The current telecom call ID. Used when creating new instances of {@link Call}. Should
+ * only be accessed using the {@link #getNextCallId()} method which synchronizes on the
+ * {@link #mLock} sync root.
+ */
+ private int mCallId = 0;
+
private final ConnectionServiceRepository mConnectionServiceRepository;
private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
private final InCallController mInCallController;
@@ -122,6 +152,7 @@
new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
private final HeadsetMediaButton mHeadsetMediaButton;
private final WiredHeadsetManager mWiredHeadsetManager;
+ private final BluetoothManager mBluetoothManager;
private final DockManager mDockManager;
private final TtyManager mTtyManager;
private final ProximitySensorManager mProximitySensorManager;
@@ -140,12 +171,6 @@
private boolean mCanAddCall = true;
- /**
- * The call the user is currently interacting with. This is the call that should have audio
- * focus and be visible in the in-call UI.
- */
- private Call mForegroundCall;
-
private Runnable mStopTone;
/**
@@ -160,7 +185,11 @@
PhoneAccountRegistrar phoneAccountRegistrar,
HeadsetMediaButtonFactory headsetMediaButtonFactory,
ProximitySensorManagerFactory proximitySensorManagerFactory,
- InCallWakeLockControllerFactory inCallWakeLockControllerFactory) {
+ InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
+ CallAudioManager.AudioServiceFactory audioServiceFactory,
+ BluetoothManager bluetoothManager,
+ WiredHeadsetManager wiredHeadsetManager,
+ SystemStateProvider systemStateProvider) {
mContext = context;
mLock = lock;
mContactsAsyncHelper = contactsAsyncHelper;
@@ -168,38 +197,70 @@
mPhoneAccountRegistrar = phoneAccountRegistrar;
mMissedCallNotifier = missedCallNotifier;
StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
- mWiredHeadsetManager = new WiredHeadsetManager(context);
+ mWiredHeadsetManager = wiredHeadsetManager;
+ mBluetoothManager = bluetoothManager;
mDockManager = new DockManager(context);
- mCallAudioManager = new CallAudioManager(
- context, mLock, statusBarNotifier, mWiredHeadsetManager, mDockManager, this);
- InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager, lock);
- mRinger = new Ringer(mCallAudioManager, this, playerFactory, context);
+
+ mDtmfLocalTonePlayer = new DtmfLocalTonePlayer();
+ CallAudioRouteStateMachine callAudioRouteStateMachine = new CallAudioRouteStateMachine(
+ context,
+ this,
+ bluetoothManager,
+ wiredHeadsetManager,
+ statusBarNotifier,
+ audioServiceFactory,
+ CallAudioRouteStateMachine.doesDeviceSupportEarpieceRoute()
+ );
+ callAudioRouteStateMachine.initialize();
+
+ CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
+ new CallAudioRoutePeripheralAdapter(
+ callAudioRouteStateMachine,
+ bluetoothManager,
+ wiredHeadsetManager,
+ mDockManager);
+
+ InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(
+ callAudioRoutePeripheralAdapter, lock);
+
+ SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
+ RingtoneFactory ringtoneFactory = new RingtoneFactory(context);
+ SystemVibrator systemVibrator = new SystemVibrator(context);
+ AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
+ mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
+ ringtoneFactory, systemVibrator);
+
+ mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
+ this,new CallAudioModeStateMachine((AudioManager)
+ mContext.getSystemService(Context.AUDIO_SERVICE)),
+ playerFactory, mRinger, new RingbackPlayer(playerFactory), mDtmfLocalTonePlayer);
+
mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
mTtyManager = new TtyManager(context, mWiredHeadsetManager);
mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
- mCallLogManager = new CallLogManager(context);
- mInCallController = new InCallController(context, mLock, this);
- mDtmfLocalTonePlayer = new DtmfLocalTonePlayer(context);
+ mCallLogManager = new CallLogManager(context, phoneAccountRegistrar);
+ mInCallController = new InCallController(context, mLock, this, systemStateProvider);
mConnectionServiceRepository =
new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
+ mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
mListeners.add(mCallLogManager);
mListeners.add(mPhoneStateBroadcaster);
mListeners.add(mInCallController);
- mListeners.add(mRinger);
- mListeners.add(new RingbackPlayer(this, playerFactory));
- mListeners.add(new InCallToneMonitor(playerFactory, this));
mListeners.add(mCallAudioManager);
mListeners.add(missedCallNotifier);
- mListeners.add(mDtmfLocalTonePlayer);
mListeners.add(mHeadsetMediaButton);
mListeners.add(mProximitySensorManager);
- mMissedCallNotifier.updateOnStartup(
- mLock, this, mContactsAsyncHelper, mCallerInfoAsyncQueryFactory);
+ // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
+ final UserManager userManager = UserManager.get(mContext);
+ // Don't load missed call if it is run in split user model.
+ if (userManager.isPrimaryUser()) {
+ onUserSwitch(Process.myUserHandle());
+ }
}
public void setRespondViaSmsManager(RespondViaSmsManager respondViaSmsManager) {
@@ -241,18 +302,63 @@
}
@Override
- public void onSuccessfulIncomingCall(Call incomingCall) {
+ public void onSuccessfulIncomingCall(Call incomingCall, boolean shouldSendToVoicemail) {
Log.d(this, "onSuccessfulIncomingCall");
- setCallState(incomingCall, CallState.RINGING, "successful incoming call");
- if (hasMaximumRingingCalls() || hasMaximumDialingCalls()) {
- incomingCall.reject(false, null);
- // since the call was not added to the list of calls, we have to call the missed
- // call notifier and the call logger manually.
- mMissedCallNotifier.showMissedCallNotification(incomingCall);
- mCallLogManager.logCall(incomingCall, Calls.MISSED_TYPE);
+ // TODO: Parallelize Call screening, block check, and send to voicemail.
+ final String number = incomingCall.getHandle() == null ? null : incomingCall.getHandle()
+ .getSchemeSpecificPart();
+ Log.v(this, "Looking up information for: %s.", Log.piiHandle(number));
+
+ new AsyncBlockCheckTask(mContext, incomingCall,
+ new CallScreening(mContext, CallsManager.this, mLock,
+ mPhoneAccountRegistrar, incomingCall), this, shouldSendToVoicemail)
+ .execute(number);
+ }
+
+ @Override
+ public void onCallScreeningCompleted(
+ Call incomingCall,
+ boolean shouldAllowCall,
+ boolean shouldReject,
+ boolean shouldAddToCallLog,
+ boolean shouldShowNotification) {
+ // Only set the incoming call as ringing if it isn't already disconnected. It is possible
+ // that the connection service disconnected the call before it was even added to Telecom, in
+ // which case it makes no sense to set it back to a ringing state.
+ if (incomingCall.getState() != CallState.DISCONNECTED &&
+ incomingCall.getState() != CallState.DISCONNECTING) {
+ setCallState(incomingCall, CallState.RINGING,
+ shouldAllowCall ? "blocking call" : "successful incoming call");
} else {
- addCall(incomingCall);
+ Log.i(this, "onCallScreeningCompleted: call already disconnected.");
+ }
+
+ if (shouldAllowCall) {
+ if (hasMaximumRingingCalls()) {
+ Log.i(this, "onCallScreeningCompleted: Call rejected! Exceeds maximum number of " +
+ "ringing calls.");
+ rejectCallAndLog(incomingCall);
+ } else if (hasMaximumDialingCalls()) {
+ Log.i(this, "onCallScreeningCompleted: Call rejected! Exceeds maximum number of " +
+ "dialing calls.");
+ rejectCallAndLog(incomingCall);
+ } else {
+ addCall(incomingCall);
+ }
+ } else {
+ if (shouldReject) {
+ Log.i(this, "onCallScreeningCompleted: blocked call, rejecting.");
+ incomingCall.reject(false, null);
+ }
+ if (shouldAddToCallLog) {
+ Log.i(this, "onCallScreeningCompleted: blocked call, adding to call log.");
+ mCallLogManager.logCall(incomingCall, Calls.MISSED_TYPE);
+ }
+ if (shouldShowNotification) {
+ Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
+ mMissedCallNotifier.showMissedCallNotification(incomingCall);
+ }
}
}
@@ -294,30 +400,32 @@
// Play tone if it is one of the dialpad digits, canceling out the previously queued
// up stopTone runnable since playing a new tone automatically stops the previous tone.
if (mStopTone != null) {
- mHandler.removeCallbacks(mStopTone);
+ mHandler.removeCallbacks(mStopTone.getRunnableToCancel());
+ mStopTone.cancel();
}
mDtmfLocalTonePlayer.playTone(call, nextChar);
// TODO: Create a LockedRunnable class that does the synchronization automatically.
- mStopTone = new Runnable() {
+ mStopTone = new Runnable("CM.oPDC") {
@Override
- public void run() {
+ public void loggedRun() {
synchronized (mLock) {
- // Set a timeout to stop the tone in case there isn't another tone to follow.
+ // Set a timeout to stop the tone in case there isn't another tone to
+ // follow.
mDtmfLocalTonePlayer.stopTone(call);
}
}
};
- mHandler.postDelayed(
- mStopTone,
+ mHandler.postDelayed(mStopTone.prepare(),
Timeouts.getDelayBetweenDtmfTonesMillis(mContext.getContentResolver()));
} else if (nextChar == 0 || nextChar == TelecomManager.DTMF_CHARACTER_WAIT ||
nextChar == TelecomManager.DTMF_CHARACTER_PAUSE) {
// Stop the tone if a tone is playing, removing any other stopTone callbacks since
// the previous tone is being stopped anyway.
if (mStopTone != null) {
- mHandler.removeCallbacks(mStopTone);
+ mHandler.removeCallbacks(mStopTone.getRunnableToCancel());
+ mStopTone.cancel();
}
mDtmfLocalTonePlayer.stopTone(call);
} else {
@@ -360,9 +468,9 @@
@Override
public boolean onCanceledViaNewOutgoingCallBroadcast(final Call call) {
mPendingCallsToDisconnect.add(call);
- mHandler.postDelayed(new Runnable() {
+ mHandler.postDelayed(new Runnable("CM.oCVNOCB") {
@Override
- public void run() {
+ public void loggedRun() {
synchronized (mLock) {
if (mPendingCallsToDisconnect.remove(call)) {
Log.i(this, "Delayed disconnection of call: %s", call);
@@ -370,7 +478,7 @@
}
}
}
- }, Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver()));
+ }.prepare(), Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver()));
return true;
}
@@ -414,16 +522,22 @@
}
}
- Collection<Call> getCalls() {
+ @VisibleForTesting
+ public Collection<Call> getCalls() {
return Collections.unmodifiableCollection(mCalls);
}
- Call getForegroundCall() {
- return mForegroundCall;
+ @VisibleForTesting
+ public Call getForegroundCall() {
+ if (mCallAudioManager == null) {
+ // Happens when getForegroundCall is called before full initialization.
+ return null;
+ }
+ return mCallAudioManager.getForegroundCall();
}
- Ringer getRinger() {
- return mRinger;
+ CallAudioManager getCallAudioManager() {
+ return mCallAudioManager;
}
InCallController getInCallController() {
@@ -469,7 +583,8 @@
return mTtyManager.getCurrentTtyMode();
}
- void addListener(CallsManagerListener listener) {
+ @VisibleForTesting
+ public void addListener(CallsManagerListener listener) {
mListeners.add(listener);
}
@@ -492,6 +607,7 @@
handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
Call call = new Call(
+ getNextCallId(),
mContext,
this,
mLock,
@@ -502,8 +618,16 @@
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccountHandle,
- true /* isIncoming */,
- false /* isConference */);
+ Call.CALL_DIRECTION_INCOMING /* callDirection */,
+ false /* forceAttachToExistingConnection */,
+ false /* isConference */
+ );
+
+ call.initAnalytics();
+ if (getForegroundCall() != null) {
+ getForegroundCall().getAnalytics().setCallIsInterrupted(true);
+ call.getAnalytics().setCallIsAdditional(true);
+ }
call.setIntentExtras(extras);
// TODO: Move this to be a part of addCall()
@@ -515,6 +639,7 @@
Uri handle = extras.getParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE);
Log.i(this, "addNewUnknownCall with handle: %s", Log.pii(handle));
Call call = new Call(
+ getNextCallId(),
mContext,
this,
mLock,
@@ -525,11 +650,14 @@
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccountHandle,
+ Call.CALL_DIRECTION_UNKNOWN /* callDirection */,
// Use onCreateIncomingConnection in TelephonyConnectionService, so that we attach
// to the existing connection instead of trying to create a new one.
- true /* isIncoming */,
- false /* isConference */);
- call.setIsUnknown(true);
+ true /* forceAttachToExistingConnection */,
+ false /* isConference */
+ );
+ call.initAnalytics();
+
call.setIntentExtras(extras);
call.addListener(this);
call.startCreateConnection(mPhoneAccountRegistrar);
@@ -549,8 +677,8 @@
return TextUtils.equals(number1, number2);
}
- private Call getNewOutgoingCall(Uri handle) {
- // First check to see if we can reuse any of the calls that are waiting to disconnect.
+ private Call reuseOutgoingCall(Uri handle) {
+ // Check to see if we can reuse any of the calls that are waiting to disconnect.
// See {@link Call#abort} and {@link #onCanceledViaNewOutgoingCall} for more information.
Call reusedCall = null;
for (Call pendingCall : mPendingCallsToDisconnect) {
@@ -563,25 +691,8 @@
pendingCall.disconnect();
}
}
- if (reusedCall != null) {
- return reusedCall;
- }
- // 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.
- return new Call(
- mContext,
- this,
- mLock,
- mConnectionServiceRepository,
- mContactsAsyncHelper,
- mCallerInfoAsyncQueryFactory,
- handle,
- null /* gatewayInfo */,
- null /* connectionManagerPhoneAccount */,
- null /* phoneAccountHandle */,
- false /* isIncoming */,
- false /* isConference */);
+ return reusedCall;
}
/**
@@ -591,17 +702,44 @@
* @param phoneAccountHandle The phone account which contains the component name of the
* connection service to use for this call.
* @param extras The optional extras Bundle passed with the intent used for the incoming call.
+ * @param initiatingUser {@link UserHandle} of user that place the outgoing call.
*/
- Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras) {
- Call call = getNewOutgoingCall(handle);
+ Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras,
+ UserHandle initiatingUser) {
+ boolean isReusedCall = true;
+ Call call = reuseOutgoingCall(handle);
+
+ // 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,
+ this,
+ mLock,
+ mConnectionServiceRepository,
+ mContactsAsyncHelper,
+ mCallerInfoAsyncQueryFactory,
+ handle,
+ null /* gatewayInfo */,
+ null /* connectionManagerPhoneAccount */,
+ null /* phoneAccountHandle */,
+ Call.CALL_DIRECTION_OUTGOING /* callDirection */,
+ false /* forceAttachToExistingConnection */,
+ false /* isConference */
+ );
+ call.setInitiatingUser(initiatingUser);
+
+ call.initAnalytics();
+
+ isReusedCall = false;
+ }
List<PhoneAccountHandle> accounts =
- mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false);
-
+ mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false,
+ initiatingUser);
Log.v(this, "startOutgoingCall found accounts = " + accounts);
- if (mForegroundCall != null) {
- Call ongoingCall = mForegroundCall;
+ if (getForegroundCall() != null) {
+ Call ongoingCall = getForegroundCall();
// If there is an ongoing call, use the same phone account to place this new call.
// If the ongoing call is a conference call, we fetch the phone account from the
// child calls because we don't have targetPhoneAccount set on Conference calls.
@@ -628,7 +766,8 @@
// No preset account, check if default exists that supports the URI scheme for the
// handle.
phoneAccountHandle =
- mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(handle.getScheme());
+ mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(handle.getScheme(),
+ initiatingUser);
}
call.setTargetPhoneAccount(phoneAccountHandle);
@@ -636,13 +775,16 @@
boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle);
// Do not support any more live calls. Our options are to move a call to hold, disconnect
- // a call, or cancel this call altogether.
- if (!isPotentialInCallMMICode && !makeRoomForOutgoingCall(call, call.isEmergencyCall())) {
+ // a call, or cancel this call altogether. If a call is being reused, then it has already
+ // passed the makeRoomForOutgoingCall check once and will fail the second time due to the
+ // call transitioning into the CONNECTING state.
+ if (!isPotentialInCallMMICode && (!isReusedCall &&
+ !makeRoomForOutgoingCall(call, call.isEmergencyCall()))) {
// just cancel at this point.
Log.i(this, "No remaining room for outgoing call: %s", call);
if (mCalls.contains(call)) {
// This call can already exist if it is a reused call,
- // See {@link #getNewOutgoingCall}.
+ // See {@link #reuseOutgoingCall}.
call.disconnect();
}
return null;
@@ -670,7 +812,7 @@
call.addListener(this);
} else if (!mCalls.contains(call)) {
// We check if mCalls already contains the call because we could potentially be reusing
- // a call which was previously added (See {@link #getNewOutgoingCall}).
+ // a call which was previously added (See {@link #reuseOutgoingCall}).
addCall(call);
}
@@ -686,8 +828,9 @@
* @param speakerphoneOn Whether or not to turn the speakerphone on once the call connects.
* @param videoState The desired video state for the outgoing call.
*/
- void placeOutgoingCall(Call call, Uri handle, GatewayInfo gatewayInfo, boolean speakerphoneOn,
- int videoState) {
+ @VisibleForTesting
+ public void placeOutgoingCall(Call call, Uri handle, GatewayInfo gatewayInfo,
+ boolean speakerphoneOn, int videoState) {
if (call == null) {
// don't do anything if the call no longer exists
Log.i(this, "Canceling unknown call.");
@@ -705,23 +848,26 @@
call.setHandle(uriHandle);
call.setGatewayInfo(gatewayInfo);
- // Auto-enable speakerphone if the originating intent specified to do so, or if the call
- // is a video call.
- call.setStartWithSpeakerphoneOn(speakerphoneOn || isSpeakerphoneAutoEnabled(videoState));
+
+ final boolean useSpeakerWhenDocked = mContext.getResources().getBoolean(
+ R.bool.use_speaker_when_docked);
+ final boolean isDocked = mDockManager.isDocked();
+ final boolean useSpeakerForVideoCall = isSpeakerphoneAutoEnabled(videoState);
+
+ // Auto-enable speakerphone if the originating intent specified to do so, if the call
+ // is a video call, of if using speaker when docked
+ call.setStartWithSpeakerphoneOn(speakerphoneOn || useSpeakerForVideoCall
+ || (useSpeakerWhenDocked && isDocked));
call.setVideoState(videoState);
if (speakerphoneOn) {
Log.i(this, "%s Starting with speakerphone as requested", call);
- } else {
+ } else if (useSpeakerWhenDocked && useSpeakerWhenDocked) {
Log.i(this, "%s Starting with speakerphone because car is docked.", call);
+ } else if (useSpeakerForVideoCall) {
+ Log.i(this, "%s Starting with speakerphone because its a video call.", call);
}
- final boolean useSpeakerWhenDocked = mContext.getResources().getBoolean(
- R.bool.use_speaker_when_docked);
-
- call.setStartWithSpeakerphoneOn(speakerphoneOn
- || (useSpeakerWhenDocked && mDockManager.isDocked()));
-
if (call.isEmergencyCall()) {
// Emergency -- CreateConnectionProcessor will choose accounts automatically
call.setTargetPhoneAccount(null);
@@ -735,8 +881,8 @@
// Otherwise the connection will be initiated when the account is set by the user.
call.startCreateConnection(mPhoneAccountRegistrar);
} else if (mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
- requireCallCapableAccountByHandle ? call.getHandle().getScheme() : null, false)
- .isEmpty()) {
+ requireCallCapableAccountByHandle ? call.getHandle().getScheme() : null, false,
+ call.getInitiatingUser()).isEmpty()) {
// If there are no call capable accounts, disconnect the call.
markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.CANCELED,
"No registered PhoneAccounts"));
@@ -750,7 +896,8 @@
* @param call The call to conference.
* @param otherCall The other call to conference with.
*/
- void conference(Call call, Call otherCall) {
+ @VisibleForTesting
+ public void conference(Call call, Call otherCall) {
call.conferenceWith(otherCall);
}
@@ -762,22 +909,24 @@
* @param call The call to answer.
* @param videoState The video state in which to answer the call.
*/
- void answerCall(Call call, int videoState) {
+ @VisibleForTesting
+ public void answerCall(Call call, int videoState) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to answer a non-existent call %s", call);
} else {
+ Call foregroundCall = getForegroundCall();
// If the foreground call is not the ringing call and it is currently isActive() or
// STATE_DIALING, put it on hold before answering the call.
- if (mForegroundCall != null && mForegroundCall != call &&
- (mForegroundCall.isActive() ||
- mForegroundCall.getState() == CallState.DIALING)) {
- if (0 == (mForegroundCall.getConnectionCapabilities()
+ if (foregroundCall != null && foregroundCall != call &&
+ (foregroundCall.isActive() ||
+ foregroundCall.getState() == CallState.DIALING)) {
+ if (0 == (foregroundCall.getConnectionCapabilities()
& Connection.CAPABILITY_HOLD)) {
// This call does not support hold. If it is from a different connection
// service, then disconnect it, otherwise allow the connection service to
// figure out the right states.
- if (mForegroundCall.getConnectionService() != call.getConnectionService()) {
- mForegroundCall.disconnect();
+ if (foregroundCall.getConnectionService() != call.getConnectionService()) {
+ foregroundCall.disconnect();
}
} else {
Call heldCall = getHeldCall();
@@ -788,8 +937,8 @@
}
Log.v(this, "Holding active/dialing call %s before answering incoming call %s.",
- mForegroundCall, call);
- mForegroundCall.hold();
+ foregroundCall, call);
+ foregroundCall.hold();
}
// TODO: Wait until we get confirmation of the active call being
// on-hold before answering the new call.
@@ -820,7 +969,7 @@
private boolean isSpeakerphoneAutoEnabled(int videoState) {
return VideoProfile.isVideo(videoState) &&
!mWiredHeadsetManager.isPluggedIn() &&
- !mCallAudioManager.isBluetoothDeviceAvailable() &&
+ !mBluetoothManager.isBluetoothAvailable() &&
isSpeakerEnabledForVideoCalls();
}
@@ -840,7 +989,8 @@
* app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
* the user opting to reject said call.
*/
- void rejectCall(Call call, boolean rejectWithMessage, String textMessage) {
+ @VisibleForTesting
+ public void rejectCall(Call call, boolean rejectWithMessage, String textMessage) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to reject a non-existent call %s", call);
} else {
@@ -856,7 +1006,8 @@
*
* @param digit The DTMF digit to play.
*/
- void playDtmfTone(Call call, char digit) {
+ @VisibleForTesting
+ public void playDtmfTone(Call call, char digit) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to play DTMF in a non-existent call %s", call);
} else {
@@ -868,7 +1019,8 @@
/**
* Instructs Telecom to stop the currently playing DTMF tone, if any.
*/
- void stopDtmfTone(Call call) {
+ @VisibleForTesting
+ public void stopDtmfTone(Call call) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to stop DTMF in a non-existent call %s", call);
} else {
@@ -893,7 +1045,8 @@
* in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
* the user hitting the end-call button.
*/
- void disconnectCall(Call call) {
+ @VisibleForTesting
+ public void disconnectCall(Call call) {
Log.v(this, "disconnectCall %s", call);
if (!mCalls.contains(call)) {
@@ -921,7 +1074,8 @@
* in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
* the user hitting the hold button during an active call.
*/
- void holdCall(Call call) {
+ @VisibleForTesting
+ public void holdCall(Call call) {
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to be put on hold", call);
} else {
@@ -935,7 +1089,8 @@
* the in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered
* by the user hitting the hold button during a held call.
*/
- void unholdCall(Call call) {
+ @VisibleForTesting
+ public void unholdCall(Call call) {
if (!mCalls.contains(call)) {
Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
} else {
@@ -950,6 +1105,20 @@
}
}
+ @Override
+ public void onExtrasChanged(Call call) {
+ if (call.getExtras() != null
+ && call.getExtras().containsKey(TelecomManager.EXTRA_CALL_TECHNOLOGY_TYPE)) {
+
+ Integer analyticsCallTechnology = sAnalyticsTechnologyMap.get(
+ call.getExtras().getInt(TelecomManager.EXTRA_CALL_TECHNOLOGY_TYPE));
+ if (analyticsCallTechnology == null) {
+ analyticsCallTechnology = Analytics.THIRD_PARTY_PHONE;
+ }
+ call.getAnalytics().addCallTechnology(analyticsCallTechnology);
+ }
+ }
+
/** Called by the in-call UI to change the mute state. */
void mute(boolean shouldMute) {
mCallAudioManager.mute(shouldMute);
@@ -981,14 +1150,12 @@
if (!mCalls.contains(call)) {
Log.i(this, "Attempted to add account to unknown call %s", call);
} else {
- // TODO: There is an odd race condition here. Since NewOutgoingCallIntentBroadcaster and
- // the SELECT_PHONE_ACCOUNT sequence run in parallel, if the user selects an account before the
- // NEW_OUTGOING_CALL sequence finishes, we'll start the call immediately without
- // respecting a rewritten number or a canceled number. This is unlikely since
- // NEW_OUTGOING_CALL sequence, in practice, runs a lot faster than the user selecting
- // a phone account from the in-call UI.
call.setTargetPhoneAccount(account);
+ if (!call.isNewOutgoingCallIntentBroadcastDone()) {
+ return;
+ }
+
// Note: emergency calls never go through account selection dialog so they never
// arrive here.
if (makeRoomForOutgoingCall(call, false /* isEmergencyCall */)) {
@@ -998,13 +1165,16 @@
}
if (setDefault) {
- mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(account);
+ mPhoneAccountRegistrar
+ .setUserSelectedOutgoingPhoneAccount(account, call.getInitiatingUser());
}
}
}
/** Called when the audio state changes. */
- void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+ @VisibleForTesting
+ public void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState
+ newAudioState) {
Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
for (CallsManagerListener listener : mListeners) {
listener.onCallAudioStateChanged(oldAudioState, newAudioState);
@@ -1047,8 +1217,9 @@
removeCall(call);
if (mLocallyDisconnectingCalls.contains(call)) {
mLocallyDisconnectingCalls.remove(call);
- if (mForegroundCall != null && mForegroundCall.getState() == CallState.ON_HOLD) {
- mForegroundCall.unhold();
+ Call foregroundCall = getForegroundCall();
+ if (foregroundCall != null && foregroundCall.getState() == CallState.ON_HOLD) {
+ foregroundCall.unhold();
}
}
}
@@ -1112,6 +1283,13 @@
* Returns true if telecom supports adding another top-level call.
*/
boolean canAddCall() {
+ boolean isDeviceProvisioned = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+ if (!isDeviceProvisioned) {
+ Log.d(TAG, "Device not provisioned, canAddCall is false.");
+ return false;
+ }
+
if (getFirstCallWithState(OUTGOING_CALL_STATES) != null) {
return false;
}
@@ -1150,7 +1328,8 @@
return getFirstCallWithState(CallState.RINGING);
}
- Call getActiveCall() {
+ @VisibleForTesting
+ public Call getActiveCall() {
return getFirstCallWithState(CallState.ACTIVE);
}
@@ -1158,11 +1337,13 @@
return getFirstCallWithState(CallState.DIALING);
}
- Call getHeldCall() {
+ @VisibleForTesting
+ public Call getHeldCall() {
return getFirstCallWithState(CallState.ON_HOLD);
}
- int getNumHeldCalls() {
+ @VisibleForTesting
+ public int getNumHeldCalls() {
int count = 0;
for (Call call : mCalls) {
if (call.getParentCall() == null && call.getState() == CallState.ON_HOLD) {
@@ -1172,11 +1353,13 @@
return count;
}
- Call getOutgoingCall() {
+ @VisibleForTesting
+ public Call getOutgoingCall() {
return getFirstCallWithState(OUTGOING_CALL_STATES);
}
- Call getFirstCallWithState(int... states) {
+ @VisibleForTesting
+ public Call getFirstCallWithState(int... states) {
return getFirstCallWithState(null, states);
}
@@ -1190,8 +1373,9 @@
Call getFirstCallWithState(Call callToSkip, int... states) {
for (int currentState : states) {
// check the foreground first
- if (mForegroundCall != null && mForegroundCall.getState() == currentState) {
- return mForegroundCall;
+ Call foregroundCall = getForegroundCall();
+ if (foregroundCall != null && foregroundCall.getState() == currentState) {
+ return foregroundCall;
}
for (Call call : mCalls) {
@@ -1213,6 +1397,7 @@
}
Call createConferenceCall(
+ String callId,
PhoneAccountHandle phoneAccount,
ParcelableConference parcelableConference) {
@@ -1224,6 +1409,7 @@
parcelableConference.getConnectTimeMillis();
Call call = new Call(
+ callId,
mContext,
this,
mLock,
@@ -1234,7 +1420,8 @@
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
phoneAccount,
- false /* isIncoming */,
+ Call.CALL_DIRECTION_UNDEFINED /* callDirection */,
+ false /* forceAttachToExistingConnection */,
true /* isConference */,
connectTime);
@@ -1277,6 +1464,19 @@
}
/**
+ * Reject an incoming call and manually add it to the Call Log.
+ * @param incomingCall Incoming call that has been rejected
+ */
+ private void rejectCallAndLog(Call incomingCall) {
+ incomingCall.reject(false, null);
+ // Since the call was not added to the list of calls, we have to call the missed
+ // call notifier and the call logger manually.
+ // Do we need missed call notification for direct to Voicemail calls?
+ mMissedCallNotifier.showMissedCallNotification(incomingCall);
+ mCallLogManager.logCall(incomingCall, Calls.MISSED_TYPE);
+ }
+
+ /**
* Adds the specified call to the main list of live calls.
*
* @param call The call to add.
@@ -1287,7 +1487,7 @@
call.addListener(this);
mCalls.add(call);
- // TODO: Update mForegroundCall prior to invoking
+ updateCallsManagerState();
// onCallAdded for calls which immediately take the foreground (like the first call).
for (CallsManagerListener listener : mListeners) {
if (Log.SYSTRACE_DEBUG) {
@@ -1298,7 +1498,6 @@
Trace.endSection();
}
}
- updateCallsManagerState();
Trace.endSection();
}
@@ -1320,6 +1519,7 @@
// Only broadcast changes for calls that are being tracked.
if (shouldNotify) {
+ updateCallsManagerState();
for (CallsManagerListener listener : mListeners) {
if (Log.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " onCallRemoved");
@@ -1329,7 +1529,6 @@
Trace.endSection();
}
}
- updateCallsManagerState();
}
Trace.endSection();
}
@@ -1360,6 +1559,7 @@
Trace.beginSection("onCallStateChanged");
// Only broadcast state change for calls that are being tracked.
if (mCalls.contains(call)) {
+ updateCallsManagerState();
for (CallsManagerListener listener : mListeners) {
if (Log.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " onCallStateChanged");
@@ -1369,59 +1569,11 @@
Trace.endSection();
}
}
- updateCallsManagerState();
}
Trace.endSection();
}
}
- /**
- * Checks which call should be visible to the user and have audio focus.
- */
- private void updateForegroundCall() {
- Trace.beginSection("updateForegroundCall");
- Call newForegroundCall = null;
- for (Call call : mCalls) {
- // TODO: Foreground-ness needs to be explicitly set. No call, regardless
- // of its state will be foreground by default and instead the connection service should
- // be notified when its calls enter and exit foreground state. Foreground will mean that
- // the call should play audio and listen to microphone if it wants.
-
- // Only top-level calls can be in foreground
- if (call.getParentCall() != null) {
- continue;
- }
-
- // Active calls have priority.
- if (call.isActive()) {
- newForegroundCall = call;
- break;
- }
-
- if (call.isAlive() || call.getState() == CallState.RINGING) {
- newForegroundCall = call;
- // Don't break in case there's an active call that has priority.
- }
- }
-
- if (newForegroundCall != mForegroundCall) {
- Log.v(this, "Updating foreground call, %s -> %s.", mForegroundCall, newForegroundCall);
- Call oldForegroundCall = mForegroundCall;
- mForegroundCall = newForegroundCall;
-
- for (CallsManagerListener listener : mListeners) {
- if (Log.SYSTRACE_DEBUG) {
- Trace.beginSection(listener.getClass().toString() + " updateForegroundCall");
- }
- listener.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
- if (Log.SYSTRACE_DEBUG) {
- Trace.endSection();
- }
- }
- }
- Trace.endSection();
- }
-
private void updateCanAddCall() {
boolean newCanAddCall = canAddCall();
if (newCanAddCall != mCanAddCall) {
@@ -1439,7 +1591,6 @@
}
private void updateCallsManagerState() {
- updateForegroundCall();
updateCanAddCall();
}
@@ -1509,7 +1660,7 @@
if (hasMaximumLiveCalls()) {
// NOTE: If the amount of live calls changes beyond 1, this logic will probably
// have to change.
- Call liveCall = getFirstCallWithState(call, LIVE_CALL_STATES);
+ Call liveCall = getFirstCallWithState(LIVE_CALL_STATES);
Log.i(this, "makeRoomForOutgoingCall call = " + call + " livecall = " +
liveCall);
@@ -1526,12 +1677,16 @@
// Disconnect the current outgoing call if it's not an emergency call. If the
// user tries to make two outgoing calls to different emergency call numbers,
// we will try to connect the first outgoing call.
+ call.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
outgoingCall.disconnect();
return true;
}
if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
// If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
// state, just disconnect it since the user has explicitly started a new call.
+ call.getAnalytics().setCallIsAdditional(true);
+ outgoingCall.getAnalytics().setCallIsInterrupted(true);
outgoingCall.disconnect();
return true;
}
@@ -1543,6 +1698,8 @@
if (isEmergency) {
// Kill the current active call, this is easier then trying to disconnect a
// holding call and hold an active call.
+ call.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.disconnect();
return true;
}
@@ -1572,6 +1729,8 @@
// how to handle the new call relative to the current one.
if (Objects.equals(liveCallPhoneAccount, call.getTargetPhoneAccount())) {
Log.i(this, "makeRoomForOutgoingCall: phoneAccount matches.");
+ call.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
return true;
} else if (call.getTargetPhoneAccount() == null) {
// Without a phone account, we can't say reliably that the call will fail.
@@ -1586,6 +1745,8 @@
// Try to hold the live call before attempting the new outgoing call.
if (liveCall.can(Connection.CAPABILITY_HOLD)) {
Log.i(this, "makeRoomForOutgoingCall: holding live call.");
+ call.getAnalytics().setCallIsAdditional(true);
+ liveCall.getAnalytics().setCallIsInterrupted(true);
liveCall.hold();
return true;
}
@@ -1631,6 +1792,7 @@
*/
Call createCallForExistingConnection(String callId, ParcelableConnection connection) {
Call call = new Call(
+ callId,
mContext,
this,
mLock,
@@ -1641,10 +1803,14 @@
null /* gatewayInfo */,
null /* connectionManagerPhoneAccount */,
connection.getPhoneAccount(), /* targetPhoneAccountHandle */
- false /* isIncoming */,
+ Call.CALL_DIRECTION_UNDEFINED /* callDirection */,
+ false /* forceAttachToExistingConnection */,
false /* isConference */,
connection.getConnectTimeMillis() /* connectTimeMillis */);
+ call.initAnalytics();
+ call.getAnalytics().setCreatedFromExistingConnection(true);
+
setCallState(call, Call.getStateFromConnectionState(connection.getState()),
"existing connection");
call.setConnectionCapabilities(connection.getConnectionCapabilities());
@@ -1658,6 +1824,43 @@
}
/**
+ * @return A new unique telecom call Id.
+ */
+ private String getNextCallId() {
+ synchronized(mLock) {
+ return TELECOM_CALL_ID_PREFIX + (++mCallId);
+ }
+ }
+
+ /**
+ * Callback when foreground user is switched. We will reload missed call in all profiles
+ * including the user itself. There may be chances that profiles are not started yet.
+ */
+ void onUserSwitch(UserHandle userHandle) {
+ mMissedCallNotifier.setCurrentUserHandle(userHandle);
+ final UserManager userManager = UserManager.get(mContext);
+ List<UserInfo> profiles = userManager.getEnabledProfiles(userHandle.getIdentifier());
+ for (UserInfo profile : profiles) {
+ reloadMissedCallsOfUser(profile.getUserHandle());
+ }
+ }
+
+ /**
+ * Because there may be chances that profiles are not started yet though its parent user is
+ * switched, we reload missed calls of profile that are just started here.
+ */
+ void onUserStarting(UserHandle userHandle) {
+ if (UserUtil.isProfile(mContext, userHandle)) {
+ reloadMissedCallsOfUser(userHandle);
+ }
+ }
+
+ private void reloadMissedCallsOfUser(UserHandle userHandle) {
+ mMissedCallNotifier.reloadFromDatabase(
+ mLock, this, mContactsAsyncHelper, mCallerInfoAsyncQueryFactory, userHandle);
+ }
+
+ /**
* Dumps the state of the {@link CallsManager}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
@@ -1672,7 +1875,6 @@
}
pw.decreaseIndent();
}
- pw.println("mForegroundCall: " + (mForegroundCall == null ? "none" : mForegroundCall));
if (mCallAudioManager != null) {
pw.println("mCallAudioManager:");
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 58085a0..50716d5 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -52,10 +52,6 @@
}
@Override
- public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
- }
-
- @Override
public void onCallAudioStateChanged(CallAudioState oldAudioState,
CallAudioState newAudioState) {
}
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index a587b59..4685704 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -21,6 +21,7 @@
import android.os.UserHandle;
import android.util.Pair;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.HashMap;
@@ -28,7 +29,8 @@
/**
* Searches for and returns connection services.
*/
-final class ConnectionServiceRepository {
+@VisibleForTesting
+public class ConnectionServiceRepository {
private final HashMap<Pair<ComponentName, UserHandle>, ConnectionServiceWrapper> mServiceCache =
new HashMap<>();
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
@@ -57,7 +59,8 @@
mCallsManager = callsManager;
}
- ConnectionServiceWrapper getService(ComponentName componentName, UserHandle userHandle) {
+ @VisibleForTesting
+ public ConnectionServiceWrapper getService(ComponentName componentName, UserHandle userHandle) {
Pair<ComponentName, UserHandle> cacheKey = Pair.create(componentName, userHandle);
ConnectionServiceWrapper service = mServiceCache.get(cacheKey);
if (service == null) {
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index b80134e..56fbd5c 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -37,6 +37,7 @@
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IConnectionService;
import com.android.internal.telecom.IConnectionServiceAdapter;
import com.android.internal.telecom.IVideoProvider;
@@ -57,220 +58,214 @@
* {@link IConnectionService} directly and instead should use this class to invoke methods of
* {@link IConnectionService}.
*/
-final class ConnectionServiceWrapper extends ServiceBinder {
+@VisibleForTesting
+public class ConnectionServiceWrapper extends ServiceBinder {
private final class Adapter extends IConnectionServiceAdapter.Stub {
@Override
- public void handleCreateConnectionComplete(
- String callId,
- ConnectionRequest request,
+ public void handleCreateConnectionComplete(String callId, ConnectionRequest request,
ParcelableConnection connection) {
+ Log.startSession("CSW.hCCC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("handleCreateConnectionComplete %s", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
- ConnectionServiceWrapper.this
- .handleCreateConnectionComplete(callId, request, connection);
- }
+ ConnectionServiceWrapper.this
+ .handleCreateConnectionComplete(callId, request, connection);
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setActive(String callId) {
+ Log.startSession("CSW.sA");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setActive %s", callId);
- if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper
- .isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- mCallsManager.markCallAsActive(call);
- } else {
- // Log.w(this, "setActive, unknown call id: %s", msg.obj);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ mCallsManager.markCallAsActive(call);
+ } else {
+ // Log.w(this, "setActive, unknown call id: %s", msg.obj);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setRinging(String callId) {
+ Log.startSession("CSW.sR");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setRinging %s", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- mCallsManager.markCallAsRinging(call);
- } else {
- // Log.w(this, "setRinging, unknown call id: %s", msg.obj);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ mCallsManager.markCallAsRinging(call);
+ } else {
+ // Log.w(this, "setRinging, unknown call id: %s", msg.obj);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setVideoProvider(String callId, IVideoProvider videoProvider) {
+ Log.startSession("CSW.sVP");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setVideoProvider %s", callId);
- if (mCallIdMapper.isValidCallId(callId)
- || mCallIdMapper.isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setVideoProvider(videoProvider);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setVideoProvider(videoProvider);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setDialing(String callId) {
+ Log.startSession("CSW.sD");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setDialing %s", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- mCallsManager.markCallAsDialing(call);
- } else {
- // Log.w(this, "setDialing, unknown call id: %s", msg.obj);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ mCallsManager.markCallAsDialing(call);
+ } else {
+ // Log.w(this, "setDialing, unknown call id: %s", msg.obj);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setDisconnected(String callId, DisconnectCause disconnectCause) {
+ Log.startSession("CSW.sD");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setDisconnected %s %s", callId, disconnectCause);
- if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper
- .isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- Log.d(this, "disconnect call %s %s", disconnectCause, call);
- if (call != null) {
- mCallsManager.markCallAsDisconnected(call, disconnectCause);
- } else {
- // Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ Log.d(this, "disconnect call %s %s", disconnectCause, call);
+ if (call != null) {
+ mCallsManager.markCallAsDisconnected(call, disconnectCause);
+ } else {
+ // Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setOnHold(String callId) {
+ Log.startSession("CSW.sOH");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setOnHold %s", callId);
- if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper
- .isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- mCallsManager.markCallAsOnHold(call);
- } else {
- // Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ mCallsManager.markCallAsOnHold(call);
+ } else {
+ // Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setRingbackRequested(String callId, boolean ringback) {
+ Log.startSession("CSW.SRR");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setRingbackRequested %s %b", callId, ringback);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setRingbackRequested(ringback);
- } else {
- // Log.w(this, "setRingback, unknown call id: %s", args.arg1);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setRingbackRequested(ringback);
+ } else {
+ // Log.w(this, "setRingback, unknown call id: %s", args.arg1);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void removeCall(String callId) {
+ Log.startSession("CSW.rC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("removeCall %s", callId);
- if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper
- .isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- if (call.isAlive()) {
- mCallsManager.markCallAsDisconnected(
- call, new DisconnectCause(DisconnectCause.REMOTE));
- } else {
- mCallsManager.markCallAsRemoved(call);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ if (call.isAlive()) {
+ mCallsManager.markCallAsDisconnected(
+ call, new DisconnectCause(DisconnectCause.REMOTE));
+ } else {
+ mCallsManager.markCallAsRemoved(call);
}
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setConnectionCapabilities(String callId, int connectionCapabilities) {
+ Log.startSession("CSW.sCC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setConnectionCapabilities %s %d", callId, connectionCapabilities);
- if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper
- .isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setConnectionCapabilities(connectionCapabilities);
- } else {
- // Log.w(ConnectionServiceWrapper.this,
- // "setConnectionCapabilities, unknown call id: %s", msg.obj);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setConnectionCapabilities(connectionCapabilities);
+ } else {
+ // Log.w(ConnectionServiceWrapper.this,
+ // "setConnectionCapabilities, unknown call id: %s", msg.obj);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setIsConferenced(String callId, String conferenceCallId) {
+ Log.startSession("CSW.sIC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -290,43 +285,44 @@
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setConferenceMergeFailed(String callId) {
+ Log.startSession("CSW.sCMF");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setConferenceMergeFailed %s", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
- // TODO: we should move the UI for indication a merge failure here
- // from CallNotifier.onSuppServiceFailed(). This way the InCallUI can
- // deliver the message anyway that they want. b/20530631.
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- // Just refresh the connection capabilities so that the UI
- // is forced to reenable the merge button as the capability
- // is still on the connection. Note when b/20530631 is fixed, we need
- // to revisit this fix to remove this hacky way of unhiding the merge
- // button (side effect of reprocessing the capabilities) and plumb
- // the failure event all the way to InCallUI instead of stopping
- // it here. That way we can also handle the UI of notifying that
- // the merged has failed.
- call.setConnectionCapabilities(call.getConnectionCapabilities(), true);
- } else {
- Log.w(this, "setConferenceMergeFailed, unknown call id: %s", callId);
- }
+ // TODO: we should move the UI for indication a merge failure here
+ // from CallNotifier.onSuppServiceFailed(). This way the InCallUI can
+ // deliver the message anyway that they want. b/20530631.
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ // Just refresh the connection capabilities so that the UI
+ // is forced to reenable the merge button as the capability
+ // is still on the connection. Note when b/20530631 is fixed, we need
+ // to revisit this fix to remove this hacky way of unhiding the merge
+ // button (side effect of reprocessing the capabilities) and plumb
+ // the failure event all the way to InCallUI instead of stopping
+ // it here. That way we can also handle the UI of notifying that
+ // the merged has failed.
+ call.setConnectionCapabilities(call.getConnectionCapabilities(), true);
+ } else {
+ Log.w(this, "setConferenceMergeFailed, unknown call id: %s", callId);
}
-
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void addConferenceCall(String callId, ParcelableConference parcelableConference) {
+ Log.startSession("CSW.aCC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -358,7 +354,7 @@
parcelableConference.getPhoneAccount() != null) {
phAcc = parcelableConference.getPhoneAccount();
}
- Call conferenceCall = mCallsManager.createConferenceCall(
+ Call conferenceCall = mCallsManager.createConferenceCall(callId,
phAcc, parcelableConference);
mCallIdMapper.addCall(conferenceCall, callId);
conferenceCall.setConnectionService(ConnectionServiceWrapper.this);
@@ -375,206 +371,208 @@
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void onPostDialWait(String callId, String remaining) throws RemoteException {
+ Log.startSession("CSW.oPDW");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("onPostDialWait %s %s", callId, remaining);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.onPostDialWait(remaining);
- } else {
- // Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.onPostDialWait(remaining);
+ } else {
+ // Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void onPostDialChar(String callId, char nextChar) throws RemoteException {
+ Log.startSession("CSW.oPDC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("onPostDialChar %s %s", callId, nextChar);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.onPostDialChar(nextChar);
- } else {
- // Log.w(this, "onPostDialChar, unknown call id: %s", args.arg1);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.onPostDialChar(nextChar);
+ } else {
+ // Log.w(this, "onPostDialChar, unknown call id: %s", args.arg1);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void queryRemoteConnectionServices(RemoteServiceCallback callback) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ Log.startSession("CSW.qRCS");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("queryRemoteConnectionServices %s", callback);
- ConnectionServiceWrapper.this.queryRemoteConnectionServices(callback);
+ ConnectionServiceWrapper.this
+ .queryRemoteConnectionServices(callingUserHandle, callback);
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setVideoState(String callId, int videoState) {
+ Log.startSession("CSW.sVS");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setVideoState %s %d", callId, videoState);
- if (mCallIdMapper.isValidCallId(callId)
- || mCallIdMapper.isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setVideoState(videoState);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setVideoState(videoState);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setIsVoipAudioMode(String callId, boolean isVoip) {
+ Log.startSession("CSW.sIVAM");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setIsVoipAudioMode %s %b", callId, isVoip);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setIsVoipAudioMode(isVoip);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setIsVoipAudioMode(isVoip);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setStatusHints(String callId, StatusHints statusHints) {
+ Log.startSession("CSW.sSH");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setStatusHints %s %s", callId, statusHints);
- if (mCallIdMapper.isValidCallId(callId)
- || mCallIdMapper.isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setStatusHints(statusHints);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setStatusHints(statusHints);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setExtras(String callId, Bundle extras) {
+ Log.startSession("CSW.sE");
long token = Binder.clearCallingIdentity();
try {
- synchronized(mLock) {
+ synchronized (mLock) {
logIncoming("setExtras %s %s", callId, extras);
- if (mCallIdMapper.isValidCallId(callId)
- || mCallIdMapper.isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setExtras(extras);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setExtras(extras);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setAddress(String callId, Uri address, int presentation) {
+ Log.startSession("CSW.sA");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setAddress %s %s %d", callId, address, presentation);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setHandle(address, presentation);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setHandle(address, presentation);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setCallerDisplayName(
String callId, String callerDisplayName, int presentation) {
+ Log.startSession("CSW.sCDN");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setCallerDisplayName %s %s %d", callId, callerDisplayName,
presentation);
- if (mCallIdMapper.isValidCallId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- call.setCallerDisplayName(callerDisplayName, presentation);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ call.setCallerDisplayName(callerDisplayName, presentation);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setConferenceableConnections(
String callId, List<String> conferenceableCallIds) {
+ Log.startSession("CSW.sCC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
logIncoming("setConferenceableConnections %s %s", callId,
conferenceableCallIds);
- if (mCallIdMapper.isValidCallId(callId) ||
- mCallIdMapper.isValidConferenceId(callId)) {
- Call call = mCallIdMapper.getCall(callId);
- if (call != null) {
- List<Call> conferenceableCalls =
- new ArrayList<>(conferenceableCallIds.size());
- for (String otherId : conferenceableCallIds) {
- Call otherCall = mCallIdMapper.getCall(otherId);
- if (otherCall != null && otherCall != call) {
- conferenceableCalls.add(otherCall);
- }
+ Call call = mCallIdMapper.getCall(callId);
+ if (call != null) {
+ List<Call> conferenceableCalls =
+ new ArrayList<>(conferenceableCallIds.size());
+ for (String otherId : conferenceableCallIds) {
+ Call otherCall = mCallIdMapper.getCall(otherId);
+ if (otherCall != null && otherCall != call) {
+ conferenceableCalls.add(otherCall);
}
- call.setConferenceableCalls(conferenceableCalls);
}
+ call.setConferenceableCalls(conferenceableCalls);
}
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void addExistingConnection(String callId, ParcelableConnection connection) {
+ Log.startSession("CSW.aEC");
long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
@@ -586,12 +584,13 @@
}
} finally {
Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
private final Adapter mAdapter = new Adapter();
- private final CallIdMapper mCallIdMapper = new CallIdMapper("ConnectionService");
+ private final CallIdMapper mCallIdMapper = new CallIdMapper();
private final Map<String, CreateConnectionResponse> mPendingResponses = new HashMap<>();
private Binder2 mBinder = new Binder2();
@@ -642,7 +641,8 @@
/**
* Creates a new connection for a new outgoing call or to attach to an existing incoming call.
*/
- void createConnection(final Call call, final CreateConnectionResponse response) {
+ @VisibleForTesting
+ public void createConnection(final Call call, final CreateConnectionResponse response) {
Log.d(this, "createConnection(%s) via %s.", call, getComponentName());
BindCallback callback = new BindCallback() {
@Override
@@ -672,8 +672,9 @@
call.getTargetPhoneAccount(),
call.getHandle(),
extras,
- call.getVideoState()),
- call.isIncoming(),
+ call.getVideoState(),
+ callId),
+ call.shouldAttachToExistingConnection(),
call.isUnknown());
} catch (RemoteException e) {
Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
@@ -709,6 +710,18 @@
removeCall(call, new DisconnectCause(DisconnectCause.LOCAL));
}
+ /** @see IConnectionService#silence(String) */
+ void silence(Call call) {
+ final String callId = mCallIdMapper.getCallId(call);
+ if (callId != null && isServiceValid("silence")) {
+ try {
+ logOutgoing("silence %s", callId);
+ mServiceInterface.silence(callId);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
/** @see IConnectionService#hold(String) */
void hold(Call call) {
final String callId = mCallIdMapper.getCallId(call);
@@ -733,8 +746,9 @@
}
}
- /** @see IConnectionService#onCallAudioStateChanged(String,CallAudioState) */
- void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
+ /** @see IConnectionService#onCallAudioStateChanged(String, CallAudioState) */
+ @VisibleForTesting
+ public void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
final String callId = mCallIdMapper.getCallId(activeCall);
if (callId != null && isServiceValid("onCallAudioStateChanged")) {
try {
@@ -791,7 +805,7 @@
}
}
- /** @see IConnectionService#playDtmfTone(String,char) */
+ /** @see IConnectionService#playDtmfTone(String, char) */
void playDtmfTone(Call call, char digit) {
final String callId = mCallIdMapper.getCallId(call);
if (callId != null && isServiceValid("playDtmfTone")) {
@@ -808,7 +822,7 @@
final String callId = mCallIdMapper.getCallId(call);
if (callId != null && isServiceValid("stopDtmfTone")) {
try {
- logOutgoing("stopDtmfTone %s",callId);
+ logOutgoing("stopDtmfTone %s", callId);
mServiceInterface.stopDtmfTone(callId);
} catch (RemoteException e) {
}
@@ -968,10 +982,11 @@
Log.d(this, "Telecom -> ConnectionService: " + msg, params);
}
- private void queryRemoteConnectionServices(final RemoteServiceCallback callback) {
+ private void queryRemoteConnectionServices(final UserHandle userHandle,
+ final RemoteServiceCallback callback) {
// Only give remote connection services to this connection service if it is listed as
// the connection manager.
- PhoneAccountHandle simCallManager = mPhoneAccountRegistrar.getSimCallManager();
+ PhoneAccountHandle simCallManager = mPhoneAccountRegistrar.getSimCallManager(userHandle);
Log.d(this, "queryRemoteConnectionServices finds simCallManager = %s", simCallManager);
if (simCallManager == null ||
!simCallManager.getComponentName().equals(getComponentName())) {
@@ -982,7 +997,7 @@
// Make a list of ConnectionServices that are listed as being associated with SIM accounts
final Set<ConnectionServiceWrapper> simServices = Collections.newSetFromMap(
new ConcurrentHashMap<ConnectionServiceWrapper, Boolean>(8, 0.9f, 1));
- for (PhoneAccountHandle handle : mPhoneAccountRegistrar.getSimPhoneAccounts()) {
+ for (PhoneAccountHandle handle : mPhoneAccountRegistrar.getSimPhoneAccounts(userHandle)) {
ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
handle.getComponentName(), handle.getUserHandle());
if (service != null) {
diff --git a/src/com/android/server/telecom/ContactsAsyncHelper.java b/src/com/android/server/telecom/ContactsAsyncHelper.java
index 44fa654..974fc51 100644
--- a/src/com/android/server/telecom/ContactsAsyncHelper.java
+++ b/src/com/android/server/telecom/ContactsAsyncHelper.java
@@ -29,6 +29,7 @@
// TODO: Needed for move to system service: import com.android.internal.R;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -57,15 +58,22 @@
Object cookie);
}
+ /**
+ * Interface to enable stubbing of the call to openInputStream
+ */
+ public interface ContentResolverAdapter {
+ InputStream openInputStream(Context context, Uri uri) throws FileNotFoundException;
+ }
+
// constants
private static final int EVENT_LOAD_IMAGE = 1;
/** Handler run on a worker thread to load photo asynchronously. */
private Handler mThreadHandler;
- private final TelecomSystem.SyncRoot mLock;
+ private final ContentResolverAdapter mContentResolverAdapter;
- public ContactsAsyncHelper(TelecomSystem.SyncRoot lock) {
- mLock = lock;
+ public ContactsAsyncHelper(ContentResolverAdapter contentResolverAdapter) {
+ mContentResolverAdapter = contentResolverAdapter;
}
private static final class WorkerArgs {
@@ -95,8 +103,8 @@
InputStream inputStream = null;
try {
try {
- inputStream = args.context.getContentResolver()
- .openInputStream(args.displayPhotoUri);
+ inputStream = mContentResolverAdapter.openInputStream(
+ args.context, args.displayPhotoUri);
} catch (Exception e) {
Log.e(this, e, "Error opening photo input stream");
}
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index b846470..d82240e 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -17,23 +17,21 @@
package com.android.server.telecom;
import android.content.Context;
+import android.os.UserHandle;
import android.telecom.DisconnectCause;
import android.telecom.ParcelableConnection;
-import android.telecom.Phone;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
-import android.telephony.PhoneStateListener;
-import android.telephony.ServiceState;
// TODO: Needed for move to system service: import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
-import java.util.Set;
import java.util.Objects;
/**
@@ -43,7 +41,8 @@
* to the user
* - a connection service cancels the process, in which case the call is aborted
*/
-final class CreateConnectionProcessor {
+@VisibleForTesting
+public class CreateConnectionProcessor implements CreateConnectionResponse {
// Describes information required to attempt to make a phone call
private static class CallAttemptRecord {
@@ -91,33 +90,35 @@
private final ConnectionServiceRepository mRepository;
private List<CallAttemptRecord> mAttemptRecords;
private Iterator<CallAttemptRecord> mAttemptRecordIterator;
- private CreateConnectionResponse mResponse;
+ private CreateConnectionResponse mCallResponse;
private DisconnectCause mLastErrorDisconnectCause;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final Context mContext;
- private boolean mShouldUseConnectionManager = true;
private CreateConnectionTimeout mTimeout;
+ private ConnectionServiceWrapper mService;
- CreateConnectionProcessor(
+ @VisibleForTesting
+ public CreateConnectionProcessor(
Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
mCall = call;
mRepository = repository;
- mResponse = response;
+ mCallResponse = response;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mContext = context;
}
boolean isProcessingComplete() {
- return mResponse == null;
+ return mCallResponse == null;
}
boolean isCallTimedOut() {
return mTimeout != null && mTimeout.isCallTimedOut();
}
- void process() {
+ @VisibleForTesting
+ public void process() {
Log.v(this, "process");
clearTimeout();
mAttemptRecords = new ArrayList<>();
@@ -138,7 +139,7 @@
void continueProcessingIfPossible(CreateConnectionResponse response,
DisconnectCause disconnectCause) {
Log.v(this, "continueProcessingIfPossible");
- mResponse = response;
+ mCallResponse = response;
mLastErrorDisconnectCause = disconnectCause;
attemptNextPhoneAccount();
}
@@ -148,8 +149,8 @@
// Clear the response first to prevent attemptNextConnectionService from attempting any
// more services.
- CreateConnectionResponse response = mResponse;
- mResponse = null;
+ CreateConnectionResponse response = mCallResponse;
+ mCallResponse = null;
clearTimeout();
ConnectionServiceWrapper service = mCall.getConnectionService();
@@ -190,33 +191,27 @@
}
}
- if (mResponse != null && attempt != null) {
+ if (mCallResponse != null && attempt != null) {
Log.i(this, "Trying attempt %s", attempt);
PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
- ConnectionServiceWrapper service =
- mRepository.getService(
- phoneAccount.getComponentName(),
- phoneAccount.getUserHandle());
- if (service == null) {
+ mService = mRepository.getService(phoneAccount.getComponentName(),
+ phoneAccount.getUserHandle());
+ if (mService == null) {
Log.i(this, "Found no connection service for attempt %s", attempt);
attemptNextPhoneAccount();
} else {
mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
- mCall.setConnectionService(service);
- setTimeoutIfNeeded(service, attempt);
+ mCall.setConnectionService(mService);
+ setTimeoutIfNeeded(mService, attempt);
- service.createConnection(mCall, new Response(service));
+ mService.createConnection(mCall, this);
}
} else {
Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
- if (mResponse != null) {
- clearTimeout();
- mResponse.handleCreateConnectionFailure(mLastErrorDisconnectCause != null ?
- mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR));
- mResponse = null;
- mCall.clearConnectionService();
- }
+ DisconnectCause disconnectCause = mLastErrorDisconnectCause != null ?
+ mLastErrorDisconnectCause : new DisconnectCause(DisconnectCause.ERROR);
+ notifyCallConnectionFailure(disconnectCause);
}
}
@@ -240,10 +235,6 @@
}
private boolean shouldSetConnectionManager() {
- if (!mShouldUseConnectionManager) {
- return false;
- }
-
if (mAttemptRecords.size() == 0) {
return false;
}
@@ -254,7 +245,8 @@
return false;
}
- PhoneAccountHandle connectionManager = mPhoneAccountRegistrar.getSimCallManager();
+ PhoneAccountHandle connectionManager =
+ mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
if (connectionManager == null) {
return false;
}
@@ -266,8 +258,8 @@
// Connection managers are only allowed to manage SIM subscriptions.
// TODO: Should this really be checking the "calling user" test for phone account?
- PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccountCheckCallingUser(
- targetPhoneAccountHandle);
+ PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar
+ .getPhoneAccountUnchecked(targetPhoneAccountHandle);
if (targetPhoneAccount == null) {
Log.d(this, "shouldSetConnectionManager, phone account not found");
return false;
@@ -285,10 +277,10 @@
private void adjustAttemptsForConnectionManager() {
if (shouldSetConnectionManager()) {
CallAttemptRecord record = new CallAttemptRecord(
- mPhoneAccountRegistrar.getSimCallManager(),
+ mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall),
mAttemptRecords.get(0).targetPhoneAccount);
Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
- mAttemptRecords.set(0, record);
+ mAttemptRecords.add(0, record);
} else {
Log.v(this, "setConnectionManager, not changing");
}
@@ -296,11 +288,14 @@
// If we are possibly attempting to call a local emergency number, ensure that the
// plain PSTN connection services are listed, and nothing else.
- private void adjustAttemptsForEmergency() {
+ private void adjustAttemptsForEmergency() {
if (mCall.isEmergencyCall()) {
Log.i(this, "Emergency number detected");
mAttemptRecords.clear();
- List<PhoneAccount> allAccounts = mPhoneAccountRegistrar.getAllPhoneAccounts();
+ // Phone accounts in profile do not handle emergency call, use phone accounts in
+ // current user.
+ List<PhoneAccount> allAccounts = mPhoneAccountRegistrar
+ .getAllPhoneAccountsOfCurrentUser();
if (allAccounts.isEmpty()) {
// If the list of phone accounts is empty at this point, it means Telephony hasn't
@@ -312,7 +307,6 @@
allAccounts.add(TelephonyUtil.getDefaultEmergencyPhoneAccount());
}
-
// First, add SIM phone accounts which can place emergency calls.
for (PhoneAccount phoneAccount : allAccounts) {
if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
@@ -327,18 +321,17 @@
}
// Next, add the connection manager account as a backup if it can place emergency calls.
- PhoneAccountHandle callManagerHandle = mPhoneAccountRegistrar.getSimCallManager();
- if (mShouldUseConnectionManager && callManagerHandle != null) {
+ PhoneAccountHandle callManagerHandle =
+ mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser();
+ if (callManagerHandle != null) {
// TODO: Should this really be checking the "calling user" test for phone account?
PhoneAccount callManager = mPhoneAccountRegistrar
- .getPhoneAccountCheckCallingUser(callManagerHandle);
+ .getPhoneAccountUnchecked(callManagerHandle);
if (callManager != null && callManager.hasCapabilities(
PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
- mPhoneAccountRegistrar.
- getOutgoingPhoneAccountForScheme(mCall.getHandle().getScheme())
- );
-
+ mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ mCall.getHandle().getScheme()));
if (!mAttemptRecords.contains(callAttemptRecord)) {
Log.i(this, "Will try Connection Manager account %s for emergency",
callManager);
@@ -359,69 +352,77 @@
return result;
}
- private class Response implements CreateConnectionResponse {
- private final ConnectionServiceWrapper mService;
- Response(ConnectionServiceWrapper service) {
- mService = service;
+ private void notifyCallConnectionFailure(DisconnectCause errorDisconnectCause) {
+ if (mCallResponse != null) {
+ clearTimeout();
+ mCallResponse.handleCreateConnectionFailure(errorDisconnectCause);
+ mCallResponse = null;
+ mCall.clearConnectionService();
}
+ }
- @Override
- public void handleCreateConnectionSuccess(
- CallIdMapper idMapper,
- ParcelableConnection connection) {
- if (mResponse == null) {
- // Nobody is listening for this connection attempt any longer; ask the responsible
- // ConnectionService to tear down any resources associated with the call
- mService.abort(mCall);
- } else {
- // Success -- share the good news and remember that we are no longer interested
- // in hearing about any more attempts
- mResponse.handleCreateConnectionSuccess(idMapper, connection);
- mResponse = null;
- // If there's a timeout running then don't clear it. The timeout can be triggered
- // after the call has successfully been created but before it has become active.
- }
+ @Override
+ public void handleCreateConnectionSuccess(
+ CallIdMapper idMapper,
+ ParcelableConnection connection) {
+ if (mCallResponse == null) {
+ // Nobody is listening for this connection attempt any longer; ask the responsible
+ // ConnectionService to tear down any resources associated with the call
+ mService.abort(mCall);
+ } else {
+ // Success -- share the good news and remember that we are no longer interested
+ // in hearing about any more attempts
+ mCallResponse.handleCreateConnectionSuccess(idMapper, connection);
+ mCallResponse = null;
+ // If there's a timeout running then don't clear it. The timeout can be triggered
+ // after the call has successfully been created but before it has become active.
}
+ }
- private boolean shouldFallbackToNoConnectionManager(DisconnectCause cause) {
- PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
- if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManager())) {
- return false;
- }
-
- ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
- if (connectionManager == null) {
- return false;
- }
-
- if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
- Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
- + "call, falling back to not using a connection manager");
- return true;
- }
-
- if (!connectionManager.isServiceValid("createConnection")) {
- Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
- + "create a connection, falling back to not using a connection manager");
- return true;
- }
-
+ private boolean shouldFailCallIfConnectionManagerFails(DisconnectCause cause) {
+ // Connection Manager does not exist or does not match registered Connection Manager
+ // Since Connection manager is a proxy for SIM, fall back to SIM
+ PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
+ if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManagerFromCall(
+ mCall))) {
return false;
}
- @Override
- public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
- // Failure of some sort; record the reasons for failure and try again if possible
- Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
- mLastErrorDisconnectCause = errorDisconnectCause;
- if (shouldFallbackToNoConnectionManager(errorDisconnectCause)) {
- mShouldUseConnectionManager = false;
- // Restart from the beginning.
- process();
- } else {
- attemptNextPhoneAccount();
- }
+ // The Call's Connection Service does not exist
+ ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
+ if (connectionManager == null) {
+ return true;
}
+
+ // In this case, fall back to a sim because connection manager declined
+ if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
+ Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
+ + "call, falling back to not using a connection manager");
+ return false;
+ }
+
+ if (!connectionManager.isServiceValid("createConnection")) {
+ Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
+ + "create a connection, falling back to not using a connection manager");
+ return false;
+ }
+
+ // Do not fall back from connection manager and simply fail call if the failure reason is
+ // other
+ Log.d(CreateConnectionProcessor.this, "Connection Manager denied call with the following " +
+ "error: " + cause.getReason() + ". Not falling back to SIM.");
+ return true;
+ }
+
+ @Override
+ public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
+ // Failure of some sort; record the reasons for failure and try again if possible
+ Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
+ if(shouldFailCallIfConnectionManagerFails(errorDisconnectCause)){
+ notifyCallConnectionFailure(errorDisconnectCause);
+ return;
+ }
+ attemptNextPhoneAccount();
}
}
diff --git a/src/com/android/server/telecom/CreateConnectionResponse.java b/src/com/android/server/telecom/CreateConnectionResponse.java
index 08c0cfc..8e3d0cf 100644
--- a/src/com/android/server/telecom/CreateConnectionResponse.java
+++ b/src/com/android/server/telecom/CreateConnectionResponse.java
@@ -19,10 +19,13 @@
import android.telecom.DisconnectCause;
import android.telecom.ParcelableConnection;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* A callback for providing the result of creating a connection.
*/
-interface CreateConnectionResponse {
+@VisibleForTesting
+public interface CreateConnectionResponse {
void handleCreateConnectionSuccess(CallIdMapper idMapper, ParcelableConnection connection);
void handleCreateConnectionFailure(DisconnectCause disconnectCaused);
}
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 06dc9ed..69cc129 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
+import android.os.UserHandle;
import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
@@ -28,7 +29,7 @@
/**
* Registers a timeout for a call and disconnects the call when the timeout expires.
*/
-final class CreateConnectionTimeout implements Runnable {
+final class CreateConnectionTimeout extends Runnable {
private final Context mContext;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final ConnectionServiceWrapper mConnectionService;
@@ -39,6 +40,7 @@
CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
ConnectionServiceWrapper service, Call call) {
+ super("CCT");
mContext = context;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mConnectionService = service;
@@ -54,7 +56,8 @@
// If there's no connection manager to fallback on then there's no point in having a
// timeout.
- PhoneAccountHandle connectionManager = mPhoneAccountRegistrar.getSimCallManager();
+ PhoneAccountHandle connectionManager =
+ mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
if (!accounts.contains(connectionManager)) {
return false;
}
@@ -83,7 +86,7 @@
if (timeoutLengthMillis <= 0) {
Log.d(this, "registerTimeout, timeout set to %d, skipping", timeoutLengthMillis);
} else {
- mHandler.postDelayed(this, timeoutLengthMillis);
+ mHandler.postDelayed(prepare(), timeoutLengthMillis);
}
}
@@ -91,6 +94,7 @@
Log.d(this, "unregisterTimeout");
mIsRegistered = false;
mHandler.removeCallbacksAndMessages(null);
+ cancel();
}
boolean isCallTimedOut() {
@@ -98,7 +102,7 @@
}
@Override
- public void run() {
+ public void loggedRun() {
if (mIsRegistered && isCallBeingPlaced(mCall)) {
Log.i(this, "run, call timed out, calling disconnect");
mIsCallTimedOut = true;
diff --git a/src/com/android/server/telecom/DialerCodeReceiver.java b/src/com/android/server/telecom/DialerCodeReceiver.java
new file mode 100644
index 0000000..8732222
--- /dev/null
+++ b/src/com/android/server/telecom/DialerCodeReceiver.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.TelecomManager;
+
+/**
+ * Receiver for "secret codes" broadcast by Dialer.
+ */
+public class DialerCodeReceiver extends BroadcastReceiver {
+ // Copied from TelephonyIntents.java.
+ public static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE";
+
+ // Enables extended logging for a period of time.
+ public static final String TELECOM_SECRET_CODE_DEBUG_ON = "823241";
+
+ // Disables extended logging.
+ public static final String TELECOM_SECRET_CODE_DEBUG_OFF = "823240";
+
+ // Writes a MARK to the Telecom log.
+ public static final String TELECOM_SECRET_CODE_MARK = "826275";
+
+ private final CallsManager mCallsManager;
+
+ DialerCodeReceiver(CallsManager callsManager) {
+ mCallsManager = callsManager;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (SECRET_CODE_ACTION.equals(intent.getAction()) && intent.getData() != null &&
+ intent.getData().getHost() != null) {
+ if (intent.getData().getHost().equals(TELECOM_SECRET_CODE_DEBUG_ON)) {
+ Log.i("DialerCodeReceiver", "Secret code used to enable extended logging mode");
+ Log.setIsExtendedLoggingEnabled(true);
+ } else if (intent.getData().getHost().equals(TELECOM_SECRET_CODE_DEBUG_OFF)) {
+ Log.i("DialerCodeReceiver", "Secret code used to disable extended logging mode");
+ Log.setIsExtendedLoggingEnabled(false);
+ } else if (intent.getData().getHost().equals(TELECOM_SECRET_CODE_MARK)) {
+ Log.i("DialerCodeReceiver", "Secret code used to mark logs.");
+
+ // If there is an active call, add the "log mark" for that call; otherwise we will
+ // add a non-call event.
+ Call currentCall = mCallsManager.getActiveCall();
+ Log.event(currentCall, Log.Events.USER_LOG_MARK);
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/DockManager.java b/src/com/android/server/telecom/DockManager.java
index e6ad446..27ffd28 100644
--- a/src/com/android/server/telecom/DockManager.java
+++ b/src/com/android/server/telecom/DockManager.java
@@ -21,6 +21,7 @@
import android.content.Intent;
import android.content.IntentFilter;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.Collections;
@@ -28,8 +29,10 @@
import java.util.concurrent.ConcurrentHashMap;
/** Listens for and caches car dock state. */
-class DockManager {
- interface Listener {
+@VisibleForTesting
+public class DockManager {
+ @VisibleForTesting
+ public interface Listener {
void onDockChanged(boolean isDocked);
}
@@ -37,10 +40,15 @@
private class DockBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (Intent.ACTION_DOCK_EVENT.equals(intent.getAction())) {
- int dockState = intent.getIntExtra(
- Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
- onDockChanged(dockState);
+ Log.startSession("DM.oR");
+ try {
+ if (Intent.ACTION_DOCK_EVENT.equals(intent.getAction())) {
+ int dockState = intent.getIntExtra(
+ Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
+ onDockChanged(dockState);
+ }
+ } finally {
+ Log.endSession();
}
}
}
@@ -65,7 +73,8 @@
context.registerReceiver(mReceiver, intentFilter);
}
- void addListener(Listener listener) {
+ @VisibleForTesting
+ public void addListener(Listener listener) {
mListeners.add(listener);
}
diff --git a/src/com/android/server/telecom/DtmfLocalTonePlayer.java b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
index a0d2862..20c9dd8 100644
--- a/src/com/android/server/telecom/DtmfLocalTonePlayer.java
+++ b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
@@ -34,16 +34,13 @@
* class employs a concept of a call "session" that starts and stops when the foreground call
* changes.
*/
-class DtmfLocalTonePlayer extends CallsManagerListenerBase {
+class DtmfLocalTonePlayer {
/** Generator used to actually play the tone. */
private ToneGenerator mToneGenerator;
/** The current call associated with an existing dtmf session. */
private Call mCall;
- /** The context. */
- private final Context mContext;
-
/**
* Message codes to be used for creating and deleting ToneGenerator object in the tonegenerator
* thread.
@@ -54,13 +51,8 @@
/** Handler running on the tonegenerator thread. */
private Handler mHandler;
+ public DtmfLocalTonePlayer() { }
- public DtmfLocalTonePlayer(Context context) {
- mContext = context;
- }
-
- /** {@inheritDoc} */
- @Override
public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
endDtmfSession(oldForegroundCall);
startDtmfSession(newForegroundCall);
@@ -219,7 +211,7 @@
};
}
- private static final int getMappedTone(char digit) {
+ private static int getMappedTone(char digit) {
if (digit >= '0' && digit <= '9') {
return ToneGenerator.TONE_DTMF_0 + digit - '0';
} else if (digit == '#') {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 9239288..bee03a2 100644
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -30,22 +30,25 @@
private final CallsManager mCallsManager;
private final CallIdMapper mCallIdMapper;
private final TelecomSystem.SyncRoot mLock;
+ private final String mOwnerComponentName;
/** Persists the specified parameters. */
public InCallAdapter(CallsManager callsManager, CallIdMapper callIdMapper,
- TelecomSystem.SyncRoot lock) {
+ TelecomSystem.SyncRoot lock, String ownerComponentName) {
mCallsManager = callsManager;
mCallIdMapper = callIdMapper;
mLock = lock;
+ mOwnerComponentName = ownerComponentName;
}
@Override
public void answerCall(String callId, int videoState) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- Log.d(this, "answerCall(%s,%d)", callId, videoState);
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.aC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.d(this, "answerCall(%s,%d)", callId, videoState);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.answerCall(call, videoState);
@@ -53,19 +56,22 @@
Log.w(this, "answerCall, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void rejectCall(String callId, boolean rejectWithMessage, String textMessage) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- Log.d(this, "rejectCall(%s,%b,%s)", callId, rejectWithMessage, textMessage);
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.rC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.d(this, "rejectCall(%s,%b,%s)", callId, rejectWithMessage, textMessage);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.rejectCall(call, rejectWithMessage, textMessage);
@@ -73,19 +79,22 @@
Log.w(this, "setRingback, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void playDtmfTone(String callId, char digit) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- Log.d(this, "playDtmfTone(%s,%c)", callId, digit);
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.pDT", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.d(this, "playDtmfTone(%s,%c)", callId, digit);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.playDtmfTone(call, digit);
@@ -93,19 +102,22 @@
Log.w(this, "playDtmfTone, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void stopDtmfTone(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- Log.d(this, "stopDtmfTone(%s)", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.sDT", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.d(this, "stopDtmfTone(%s)", callId);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.stopDtmfTone(call);
@@ -113,19 +125,22 @@
Log.w(this, "stopDtmfTone, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void postDialContinue(String callId, boolean proceed) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- Log.d(this, "postDialContinue(%s)", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.pDC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.d(this, "postDialContinue(%s)", callId);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.postDialContinue(call, proceed);
@@ -133,19 +148,22 @@
Log.w(this, "postDialContinue, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void disconnectCall(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- Log.v(this, "disconnectCall: %s", callId);
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.dC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ Log.v(this, "disconnectCall: %s", callId);
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.disconnectCall(call);
@@ -153,18 +171,21 @@
Log.w(this, "disconnectCall, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void holdCall(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.hC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.holdCall(call);
@@ -172,18 +193,21 @@
Log.w(this, "holdCall, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void unholdCall(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.uC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.unholdCall(call);
@@ -191,19 +215,22 @@
Log.w(this, "unholdCall, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void phoneAccountSelected(String callId, PhoneAccountHandle accountHandle,
boolean setDefault) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.pAS", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
mCallsManager.phoneAccountSelected(call, accountHandle, setDefault);
@@ -211,43 +238,55 @@
Log.w(this, "phoneAccountSelected, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void mute(boolean shouldMute) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- mCallsManager.mute(shouldMute);
+ Log.startSession("ICA.m", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallsManager.mute(shouldMute);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void setAudioRoute(int route) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- mCallsManager.setAudioRoute(route);
+ Log.startSession("ICA.sAR", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallsManager.setAudioRoute(route);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void conference(String callId, String otherCallId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId) &&
- mCallIdMapper.isValidCallId(otherCallId)) {
+ Log.startSession("ICA.c", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
Call otherCall = mCallIdMapper.getCall(otherCallId);
if (call != null && otherCall != null) {
@@ -255,20 +294,22 @@
} else {
Log.w(this, "conference, unknown call id: %s or %s", callId, otherCallId);
}
-
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void splitFromConference(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.sFC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
call.splitFromConference();
@@ -276,18 +317,21 @@
Log.w(this, "splitFromConference, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void mergeConference(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.mC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
call.mergeConference();
@@ -295,18 +339,21 @@
Log.w(this, "mergeConference, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void swapConference(String callId) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (mCallIdMapper.isValidCallId(callId)) {
+ Log.startSession("ICA.sC", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
Call call = mCallIdMapper.getCall(callId);
if (call != null) {
call.swapConference();
@@ -314,33 +361,45 @@
Log.w(this, "swapConference, unknown call id: %s", callId);
}
}
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void turnOnProximitySensor() {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- mCallsManager.turnOnProximitySensor();
+ Log.startSession("ICA.tOnPS", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallsManager.turnOnProximitySensor();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@Override
public void turnOffProximitySensor(boolean screenOnImmediately) {
- long token = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- mCallsManager.turnOffProximitySensor(screenOnImmediately);
+ Log.startSession("ICA.tOffPS", mOwnerComponentName);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallsManager.turnOffProximitySensor(screenOnImmediately);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index ab9bf91..41c99db 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -25,7 +25,6 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
-import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -33,21 +32,22 @@
import android.os.Trace;
import android.os.UserHandle;
import android.telecom.CallAudioState;
-import android.telecom.Connection;
import android.telecom.DefaultDialerManager;
import android.telecom.InCallService;
import android.telecom.ParcelableCall;
import android.telecom.TelecomManager;
-import android.telecom.VideoCallImpl;
+import android.text.TextUtils;
import android.util.ArrayMap;
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.telecom.IInCallService;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.SystemStateProvider.SystemStateListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -66,14 +66,18 @@
private class InCallServiceConnection implements ServiceConnection {
/** {@inheritDoc} */
@Override public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.startSession("ICSC.oSC");
Log.d(this, "onServiceConnected: %s", name);
onConnected(name, service);
+ Log.endSession();
}
/** {@inheritDoc} */
@Override public void onServiceDisconnected(ComponentName name) {
+ Log.startSession("ICSC.oSD");
Log.d(this, "onDisconnected: %s", name);
onDisconnected(name);
+ Log.endSession();
}
}
@@ -129,6 +133,19 @@
}
};
+ private final SystemStateListener mSystemStateListener = new SystemStateListener() {
+ @Override
+ public void onCarModeChanged(boolean isCarMode) {
+ // Do something when the car mode changes.
+ }
+ };
+
+ private static final int IN_CALL_SERVICE_TYPE_INVALID = 0;
+ private static final int IN_CALL_SERVICE_TYPE_DIALER_UI = 1;
+ private static final int IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
+ private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
+ private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
+
/**
* Maintains a binding connection to the in-call app(s).
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
@@ -146,7 +163,7 @@
*/
private ComponentName mInCallUIComponentName;
- private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall");
+ private final CallIdMapper mCallIdMapper = new CallIdMapper();
/** The {@link ComponentName} of the default InCall UI. */
private final ComponentName mSystemInCallComponentName;
@@ -154,17 +171,21 @@
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
+ private final SystemStateProvider mSystemStateProvider;
- public InCallController(
- Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager) {
+ public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
+ SystemStateProvider systemStateProvider) {
mContext = context;
mLock = lock;
mCallsManager = callsManager;
- Resources resources = mContext.getResources();
+ mSystemStateProvider = systemStateProvider;
+ Resources resources = mContext.getResources();
mSystemInCallComponentName = new ComponentName(
resources.getString(R.string.ui_default_package),
resources.getString(R.string.incall_default_class));
+
+ mSystemStateProvider.addListener(mSystemStateListener);
}
@Override
@@ -181,8 +202,8 @@
for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
ComponentName componentName = entry.getKey();
IInCallService inCallService = entry.getValue();
- ParcelableCall parcelableCall = toParcelableCall(call,
- true /* includeVideoProvider */);
+ ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
+ true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar());
try {
inCallService.addCall(parcelableCall);
} catch (RemoteException ignored) {
@@ -199,9 +220,9 @@
* give them enough time to process all the pending messages.
*/
Handler handler = new Handler(Looper.getMainLooper());
- final Runnable runnableUnbind = new Runnable() {
+ handler.postDelayed(new Runnable("ICC.oCR") {
@Override
- public void run() {
+ public void loggedRun() {
synchronized (mLock) {
// Check again to make sure there are no active calls.
if (mCallsManager.getCalls().isEmpty()) {
@@ -209,10 +230,7 @@
}
}
}
- };
- handler.postDelayed(
- runnableUnbind,
- Timeouts.getCallRemoveUnbindInCallServicesDelay(
+ }.prepare(), Timeouts.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()));
}
call.removeListener(mCallListener);
@@ -291,6 +309,17 @@
}
}
+ void silenceRinger() {
+ if (!mInCallServices.isEmpty()) {
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.silenceRinger();
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+ }
+
/**
* Unbinds an existing bound connection to the in-call app.
*/
@@ -317,82 +346,80 @@
* @param call The newly added call that triggered the binding to the in-call services.
*/
private void bindToServices(Call call) {
+ ComponentName inCallUIService = null;
+ ComponentName carModeInCallUIService = null;
+ List<ComponentName> nonUIInCallServices = new LinkedList<>();
+
+ // Loop through all the InCallService implementations that exist in the devices;
PackageManager packageManager = mContext.getPackageManager();
Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
-
- List<ComponentName> inCallControlServices = new ArrayList<>();
- ComponentName inCallUIService = null;
-
for (ResolveInfo entry :
packageManager.queryIntentServices(serviceIntent, PackageManager.GET_META_DATA)) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
- boolean hasServiceBindPermission = serviceInfo.permission != null &&
- serviceInfo.permission.equals(
- Manifest.permission.BIND_INCALL_SERVICE);
- if (!hasServiceBindPermission) {
- Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " +
- serviceInfo.packageName);
- continue;
- }
+ ComponentName componentName =
+ new ComponentName(serviceInfo.packageName, serviceInfo.name);
- boolean hasControlInCallPermission = packageManager.checkPermission(
- Manifest.permission.CONTROL_INCALL_EXPERIENCE,
- serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
- boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
- DefaultDialerManager.getDefaultDialerApplication(mContext));
- if (!hasControlInCallPermission && !isDefaultDialerPackage) {
- Log.w(this, "Service does not have CONTROL_INCALL_EXPERIENCE permission: %s"
- + " and is not system or default dialer.", serviceInfo.packageName);
- continue;
- }
+ switch (getInCallServiceType(entry.serviceInfo, packageManager)) {
+ case IN_CALL_SERVICE_TYPE_DIALER_UI:
+ if (inCallUIService == null ||
+ inCallUIService.compareTo(componentName) > 0) {
+ inCallUIService = componentName;
+ }
+ break;
- boolean isUIService = serviceInfo.metaData != null &&
- serviceInfo.metaData.getBoolean(
- TelecomManager.METADATA_IN_CALL_SERVICE_UI, false);
- ComponentName componentName = new ComponentName(serviceInfo.packageName,
- serviceInfo.name);
- if (isUIService) {
- // For the main UI service, we always prefer the default dialer.
- if (isDefaultDialerPackage) {
- inCallUIService = componentName;
- Log.i(this, "Found default-dialer's In-Call UI: %s", componentName);
- }
- } else {
- // for non-UI services that have passed our checks, add them to the list of
- // service to bind to.
- inCallControlServices.add(componentName);
- }
+ case IN_CALL_SERVICE_TYPE_SYSTEM_UI:
+ // skip, will be added manually
+ break;
+ case IN_CALL_SERVICE_TYPE_CAR_MODE_UI:
+ if (carModeInCallUIService == null ||
+ carModeInCallUIService.compareTo(componentName) > 0) {
+ carModeInCallUIService = componentName;
+ }
+ break;
+
+ case IN_CALL_SERVICE_TYPE_NON_UI:
+ nonUIInCallServices.add(componentName);
+ break;
+
+ case IN_CALL_SERVICE_TYPE_INVALID:
+ break;
+
+ default:
+ Log.w(this, "unexpected in-call service type");
+ break;
+ }
}
}
- // Attempt to bind to the default-dialer InCallService first.
- if (inCallUIService != null) {
- // skip default dialer if we have an emergency call or if it failed binding.
- if (mCallsManager.hasEmergencyCall()) {
- Log.i(this, "Skipping default-dialer because of emergency call");
- inCallUIService = null;
- } else if (!bindToInCallService(inCallUIService, call, "def-dialer")) {
- Log.event(call, Log.Events.ERROR_LOG,
- "InCallService UI failed binding: " + inCallUIService);
- inCallUIService = null;
- }
- }
+ Log.i(this, "Car mode InCallService: %s", carModeInCallUIService);
+ Log.i(this, "Dialer InCallService: %s", inCallUIService);
- if (inCallUIService == null) {
- // We failed to connect to the default-dialer service, or none was provided. Switch to
- // the system built-in InCallService UI.
- inCallUIService = mSystemInCallComponentName;
- if (!bindToInCallService(inCallUIService, call, "system")) {
- Log.event(call, Log.Events.ERROR_LOG,
- "InCallService system UI failed binding: " + inCallUIService);
- }
+ // Adding the in-call services in order:
+ // (1) The carmode in-call if carmode is on.
+ // (2) The default-dialer in-call if not an emergency call
+ // (3) The system-provided in-call
+ List<ComponentName> orderedInCallUIServices = new LinkedList<>();
+ if (shouldUseCarModeUI() && carModeInCallUIService != null) {
+ orderedInCallUIServices.add(carModeInCallUIService);
}
- mInCallUIComponentName = inCallUIService;
+ if (!mCallsManager.hasEmergencyCall() && inCallUIService != null) {
+ orderedInCallUIServices.add(inCallUIService);
+ }
+ orderedInCallUIServices.add(mSystemInCallComponentName);
+
+ // TODO: Need to implement the fall-back logic in case the main UI in-call service rejects
+ // the binding request.
+ ComponentName inCallUIServiceToBind = orderedInCallUIServices.get(0);
+ if (!bindToInCallService(inCallUIServiceToBind, call, "ui")) {
+ Log.event(call, Log.Events.ERROR_LOG,
+ "InCallService system UI failed binding: " + inCallUIService);
+ }
+ mInCallUIComponentName = inCallUIServiceToBind;
// Bind to the control InCallServices
- for (ComponentName componentName : inCallControlServices) {
+ for (ComponentName componentName : nonUIInCallServices) {
bindToInCallService(componentName, call, "control");
}
}
@@ -432,6 +459,68 @@
return false;
}
+ private boolean shouldUseCarModeUI() {
+ return mSystemStateProvider.isCarMode();
+ }
+
+ /**
+ * Returns the type of InCallService described by the specified serviceInfo.
+ */
+ private int getInCallServiceType(ServiceInfo serviceInfo, PackageManager packageManager) {
+ // Verify that the InCallService requires the BIND_INCALL_SERVICE permission which
+ // enforces that only Telecom can bind to it.
+ boolean hasServiceBindPermission = serviceInfo.permission != null &&
+ serviceInfo.permission.equals(
+ Manifest.permission.BIND_INCALL_SERVICE);
+ if (!hasServiceBindPermission) {
+ Log.w(this, "InCallService does not require BIND_INCALL_SERVICE permission: " +
+ serviceInfo.packageName);
+ return IN_CALL_SERVICE_TYPE_INVALID;
+ }
+
+ if (mSystemInCallComponentName.getPackageName().equals(serviceInfo.packageName) &&
+ mSystemInCallComponentName.getClassName().equals(serviceInfo.name)) {
+ return IN_CALL_SERVICE_TYPE_SYSTEM_UI;
+ }
+
+ // Check to see if the service is a car-mode UI type by checking that it has the
+ // CONTROL_INCALL_EXPERIENCE (to verify it is a system app) and that it has the
+ // car-mode UI metadata.
+ boolean hasControlInCallPermission = packageManager.checkPermission(
+ Manifest.permission.CONTROL_INCALL_EXPERIENCE,
+ serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
+ boolean isCarModeUIService = serviceInfo.metaData != null &&
+ serviceInfo.metaData.getBoolean(
+ TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false) &&
+ hasControlInCallPermission;
+ if (isCarModeUIService) {
+ return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
+ }
+
+
+ // Check to see that it is the default dialer package
+ boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
+ DefaultDialerManager.getDefaultDialerApplication(mContext));
+ boolean isUIService = serviceInfo.metaData != null &&
+ serviceInfo.metaData.getBoolean(
+ TelecomManager.METADATA_IN_CALL_SERVICE_UI, false);
+ if (isDefaultDialerPackage && isUIService) {
+ return IN_CALL_SERVICE_TYPE_DIALER_UI;
+ }
+
+ // Also allow any in-call service that has the control-experience permission (to ensure
+ // that it is a system app) and doesn't claim to show any UI.
+ if (hasControlInCallPermission && !isUIService) {
+ return IN_CALL_SERVICE_TYPE_NON_UI;
+ }
+
+ // Anything else that remains, we will not bind to.
+ Log.i(this, "Skipping binding to %s:%s, control: %b, car-mode: %b, ui: %b",
+ serviceInfo.packageName, serviceInfo.name, hasControlInCallPermission,
+ isCarModeUIService, isUIService);
+ return IN_CALL_SERVICE_TYPE_INVALID;
+ }
+
private void adjustServiceBindingsForEmergency() {
if (!Objects.equals(mInCallUIComponentName, mSystemInCallComponentName)) {
// The connected UI is not the system UI, so lets check if we should switch them
@@ -463,7 +552,8 @@
new InCallAdapter(
mCallsManager,
mCallIdMapper,
- mLock));
+ mLock,
+ componentName.getPackageName()));
} catch (RemoteException e) {
Log.e(this, e, "Failed to set the in-call adapter.");
Trace.endSection();
@@ -472,7 +562,7 @@
}
// Upon successful connection, send the state of the world to the service.
- Collection<Call> calls = mCallsManager.getCalls();
+ List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls());
if (!calls.isEmpty()) {
Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
componentName);
@@ -480,14 +570,18 @@
try {
// Track the call if we don't already know about it.
addCall(call);
- inCallService.addCall(toParcelableCall(call, true /* includeVideoProvider */));
+ inCallService.addCall(ParcelableCallUtils.toParcelableCall(
+ call,
+ true /* includeVideoProvider */,
+ mCallsManager.getPhoneAccountRegistrar()));
} catch (RemoteException ignored) {
}
}
- onCallAudioStateChanged(
- null,
- mCallsManager.getAudioState());
- onCanAddCallChanged(mCallsManager.canAddCall());
+ try {
+ inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
+ inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
+ } catch (RemoteException ignored) {
+ }
} else {
unbindFromServices();
}
@@ -557,8 +651,10 @@
*/
private void updateCall(Call call, boolean videoProviderChanged) {
if (!mInCallServices.isEmpty()) {
- ParcelableCall parcelableCall = toParcelableCall(call,
- videoProviderChanged /* includeVideoProvider */);
+ ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
+ call,
+ videoProviderChanged /* includeVideoProvider */,
+ mCallsManager.getPhoneAccountRegistrar());
Log.i(this, "Sending updateCall %s ==> %s", call, parcelableCall);
List<ComponentName> componentsUpdated = new ArrayList<>();
for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
@@ -575,244 +671,6 @@
}
/**
- * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
- *
- * @param call The {@link Call} to parcel.
- * @param includeVideoProvider {@code true} if the video provider should be parcelled with the
- * {@link Call}, {@code false} otherwise. Since the {@link ParcelableCall#getVideoCall()}
- * method creates a {@link VideoCallImpl} instance on access it is important for the
- * recipient of the {@link ParcelableCall} to know if the video provider changed.
- * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
- */
- private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) {
- String callId = mCallIdMapper.getCallId(call);
-
- int state = getParcelableState(call);
- int capabilities = convertConnectionToCallCapabilities(call.getConnectionCapabilities());
- int properties = convertConnectionToCallProperties(call.getConnectionCapabilities());
- if (call.isConference()) {
- properties |= android.telecom.Call.Details.PROPERTY_CONFERENCE;
- }
-
- // If this is a single-SIM device, the "default SIM" will always be the only SIM.
- boolean isDefaultSmsAccount =
- mCallsManager.getPhoneAccountRegistrar()
- .isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
- if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) {
- capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT;
- }
-
- if (call.isEmergencyCall()) {
- capabilities = removeCapability(
- capabilities, android.telecom.Call.Details.CAPABILITY_MUTE);
- }
-
- if (state == android.telecom.Call.STATE_DIALING) {
- capabilities = removeCapability(capabilities,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
- capabilities = removeCapability(capabilities,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
- }
-
- String parentCallId = null;
- Call parentCall = call.getParentCall();
- if (parentCall != null) {
- parentCallId = mCallIdMapper.getCallId(parentCall);
- }
-
- long connectTimeMillis = call.getConnectTimeMillis();
- List<Call> childCalls = call.getChildCalls();
- List<String> childCallIds = new ArrayList<>();
- if (!childCalls.isEmpty()) {
- long childConnectTimeMillis = Long.MAX_VALUE;
- for (Call child : childCalls) {
- if (child.getConnectTimeMillis() > 0) {
- childConnectTimeMillis = Math.min(child.getConnectTimeMillis(),
- childConnectTimeMillis);
- }
- childCallIds.add(mCallIdMapper.getCallId(child));
- }
-
- if (childConnectTimeMillis != Long.MAX_VALUE) {
- connectTimeMillis = childConnectTimeMillis;
- }
- }
-
- Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
- call.getHandle() : null;
- String callerDisplayName = call.getCallerDisplayNamePresentation() ==
- TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null;
-
- List<Call> conferenceableCalls = call.getConferenceableCalls();
- List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
- for (Call otherCall : conferenceableCalls) {
- String otherId = mCallIdMapper.getCallId(otherCall);
- if (otherId != null) {
- conferenceableCallIds.add(otherId);
- }
- }
-
- return new ParcelableCall(
- callId,
- state,
- call.getDisconnectCause(),
- call.getCannedSmsResponses(),
- capabilities,
- properties,
- connectTimeMillis,
- handle,
- call.getHandlePresentation(),
- callerDisplayName,
- call.getCallerDisplayNamePresentation(),
- call.getGatewayInfo(),
- call.getTargetPhoneAccount(),
- includeVideoProvider,
- includeVideoProvider ? call.getVideoProvider() : null,
- parentCallId,
- childCallIds,
- call.getStatusHints(),
- call.getVideoState(),
- conferenceableCallIds,
- call.getIntentExtras(),
- call.getExtras());
- }
-
- private static int getParcelableState(Call call) {
- int state = CallState.NEW;
- switch (call.getState()) {
- case CallState.ABORTED:
- case CallState.DISCONNECTED:
- state = android.telecom.Call.STATE_DISCONNECTED;
- break;
- case CallState.ACTIVE:
- state = android.telecom.Call.STATE_ACTIVE;
- break;
- case CallState.CONNECTING:
- state = android.telecom.Call.STATE_CONNECTING;
- break;
- case CallState.DIALING:
- state = android.telecom.Call.STATE_DIALING;
- break;
- case CallState.DISCONNECTING:
- state = android.telecom.Call.STATE_DISCONNECTING;
- break;
- case CallState.NEW:
- state = android.telecom.Call.STATE_NEW;
- break;
- case CallState.ON_HOLD:
- state = android.telecom.Call.STATE_HOLDING;
- break;
- case CallState.RINGING:
- state = android.telecom.Call.STATE_RINGING;
- break;
- case CallState.SELECT_PHONE_ACCOUNT:
- state = android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT;
- break;
- }
-
- // If we are marked as 'locally disconnecting' then mark ourselves as disconnecting instead.
- // Unless we're disconnect*ED*, in which case leave it at that.
- if (call.isLocallyDisconnecting() &&
- (state != android.telecom.Call.STATE_DISCONNECTED)) {
- state = android.telecom.Call.STATE_DISCONNECTING;
- }
- return state;
- }
-
- private static final int[] CONNECTION_TO_CALL_CAPABILITY = new int[] {
- Connection.CAPABILITY_HOLD,
- android.telecom.Call.Details.CAPABILITY_HOLD,
-
- Connection.CAPABILITY_SUPPORT_HOLD,
- android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD,
-
- Connection.CAPABILITY_MERGE_CONFERENCE,
- android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE,
-
- Connection.CAPABILITY_SWAP_CONFERENCE,
- android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE,
-
- Connection.CAPABILITY_RESPOND_VIA_TEXT,
- android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT,
-
- Connection.CAPABILITY_MUTE,
- android.telecom.Call.Details.CAPABILITY_MUTE,
-
- Connection.CAPABILITY_MANAGE_CONFERENCE,
- android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE,
-
- Connection.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
-
- Connection.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
-
- Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
-
- Connection.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
-
- Connection.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
-
- Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
- android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
-
- Connection.CAPABILITY_SEPARATE_FROM_CONFERENCE,
- android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE,
-
- Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
- android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
-
- Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
- android.telecom.Call.Details.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
-
- Connection.CAPABILITY_CAN_PAUSE_VIDEO,
- android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO,
-
- Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION,
- android.telecom.Call.Details.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION,
-
- Connection.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO,
- android.telecom.Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO
- };
-
- private static int convertConnectionToCallCapabilities(int connectionCapabilities) {
- int callCapabilities = 0;
- for (int i = 0; i < CONNECTION_TO_CALL_CAPABILITY.length; i += 2) {
- if ((CONNECTION_TO_CALL_CAPABILITY[i] & connectionCapabilities) != 0) {
- callCapabilities |= CONNECTION_TO_CALL_CAPABILITY[i + 1];
- }
- }
- return callCapabilities;
- }
-
- private static final int[] CONNECTION_TO_CALL_PROPERTIES = new int[] {
- Connection.CAPABILITY_HIGH_DEF_AUDIO,
- android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO,
-
- Connection.CAPABILITY_WIFI,
- android.telecom.Call.Details.PROPERTY_WIFI,
-
- Connection.CAPABILITY_GENERIC_CONFERENCE,
- android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE,
-
- Connection.CAPABILITY_SHOW_CALLBACK_NUMBER,
- android.telecom.Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE
- };
-
- private static int convertConnectionToCallProperties(int connectionCapabilities) {
- int callProperties = 0;
- for (int i = 0; i < CONNECTION_TO_CALL_PROPERTIES.length; i += 2) {
- if ((CONNECTION_TO_CALL_PROPERTIES[i] & connectionCapabilities) != 0) {
- callProperties |= CONNECTION_TO_CALL_PROPERTIES[i + 1];
- }
- }
- return callProperties;
- }
-
- /**
* Adds the call to the list of calls tracked by the {@link InCallController}.
* @param call The call to add.
*/
@@ -828,13 +686,6 @@
}
/**
- * Removes the specified capability from the set of capabilities bits and returns the new set.
- */
- private static int removeCapability(int capabilities, int capability) {
- return capabilities & ~capability;
- }
-
- /**
* Dumps the state of the {@link InCallController}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
@@ -854,4 +705,42 @@
}
pw.decreaseIndent();
}
+
+ static boolean doesDefaultDialerSupportRinging(Context context) {
+ String dialerPackage = DefaultDialerManager
+ .getDefaultDialerApplication(context, UserHandle.USER_CURRENT);
+ if (TextUtils.isEmpty(dialerPackage)) {
+ return false;
+ }
+
+ Intent intent = new Intent(InCallService.SERVICE_INTERFACE)
+ .setPackage(dialerPackage);
+ List<ResolveInfo> entries = context.getPackageManager()
+ .queryIntentServices(intent, PackageManager.GET_META_DATA);
+ if (entries.isEmpty()) {
+ return false;
+ }
+
+ ResolveInfo info = entries.get(0);
+ if (info.serviceInfo == null || info.serviceInfo.metaData == null) {
+ return false;
+ }
+
+ return info.serviceInfo.metaData
+ .getBoolean(TelecomManager.METADATA_IN_CALL_SERVICE_RINGING, false);
+ }
+
+ private List<Call> orderCallsWithChildrenFirst(Collection<Call> calls) {
+ LinkedList<Call> parentCalls = new LinkedList<>();
+ LinkedList<Call> childCalls = new LinkedList<>();
+ for (Call call : calls) {
+ if (call.getChildCalls().size() > 0) {
+ parentCalls.add(call);
+ } else {
+ childCalls.add(call);
+ }
+ }
+ childCalls.addAll(parentCalls);
+ return childCalls;
+ }
}
diff --git a/src/com/android/server/telecom/InCallToneMonitor.java b/src/com/android/server/telecom/InCallToneMonitor.java
deleted file mode 100644
index afe0f06..0000000
--- a/src/com/android/server/telecom/InCallToneMonitor.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2014, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.media.ToneGenerator;
-import android.telecom.Connection;
-import android.telecom.VideoProfile;
-
-import java.util.Collection;
-
-/**
- * Monitors events from CallsManager and plays in-call tones for events which require them, such as
- * different type of call disconnections (busy tone, congestion tone, etc).
- */
-public final class InCallToneMonitor extends CallsManagerListenerBase {
- private final InCallTonePlayer.Factory mPlayerFactory;
-
- private final CallsManager mCallsManager;
-
- InCallToneMonitor(InCallTonePlayer.Factory playerFactory, CallsManager callsManager) {
- mPlayerFactory = playerFactory;
- mCallsManager = callsManager;
- }
-
- @Override
- public void onCallStateChanged(Call call, int oldState, int newState) {
- if (mCallsManager.getForegroundCall() != call) {
- // We only play tones for foreground calls.
- return;
- }
-
- if (newState == CallState.DISCONNECTED && call.getDisconnectCause() != null) {
- int toneToPlay = InCallTonePlayer.TONE_INVALID;
-
- Log.v(this, "Disconnect cause: %s.", call.getDisconnectCause());
-
- switch(call.getDisconnectCause().getTone()) {
- case ToneGenerator.TONE_SUP_BUSY:
- toneToPlay = InCallTonePlayer.TONE_BUSY;
- break;
- case ToneGenerator.TONE_SUP_CONGESTION:
- toneToPlay = InCallTonePlayer.TONE_CONGESTION;
- break;
- case ToneGenerator.TONE_CDMA_REORDER:
- toneToPlay = InCallTonePlayer.TONE_REORDER;
- break;
- case ToneGenerator.TONE_CDMA_ABBR_INTERCEPT:
- toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
- break;
- case ToneGenerator.TONE_CDMA_CALLDROP_LITE:
- toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
- break;
- case ToneGenerator.TONE_SUP_ERROR:
- toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
- break;
- case ToneGenerator.TONE_PROP_PROMPT:
- toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
- break;
- }
-
- Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
-
- if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
- mPlayerFactory.createPlayer(toneToPlay).startTone();
- }
- }
- }
-
- /**
- * Handles requests received via the {@link VideoProviderProxy} requesting a change in the video
- * state of the call by the peer. If the request involves the peer turning their camera on,
- * the call waiting tone is played to inform the user of the incoming request.
- *
- * @param call The call.
- * @param videoProfile The requested video profile.
- */
- @Override
- public void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile) {
- if (videoProfile == null) {
- return;
- }
-
- if (mCallsManager.getForegroundCall() != call) {
- // We only play tones for foreground calls.
- return;
- }
-
- int previousVideoState = call.getVideoState();
- int newVideoState = videoProfile.getVideoState();
- Log.v(this, "onSessionModifyRequestReceived : videoProfile = " + VideoProfile
- .videoStateToString(newVideoState));
-
- boolean isUpgradeRequest = !VideoProfile.isReceptionEnabled(previousVideoState) &&
- VideoProfile.isReceptionEnabled(newVideoState);
-
- if (isUpgradeRequest) {
- mPlayerFactory.createPlayer(InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
- }
- }
-}
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index 0d2e3c4..78c1395 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -21,27 +21,36 @@
import android.os.Handler;
import android.os.Looper;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* Play a call-related tone (ringback, busy signal, etc.) through ToneGenerator. To use, create an
* instance using InCallTonePlayer.Factory (passing in the TONE_* constant for the tone you want)
* and start() it. Implemented on top of {@link Thread} so that the tone plays in its own thread.
*/
-public final class InCallTonePlayer extends Thread {
+public class InCallTonePlayer extends Thread {
/**
* Factory used to create InCallTonePlayers. Exists to aid with testing mocks.
*/
public static class Factory {
- private final CallAudioManager mCallAudioManager;
+ private CallAudioManager mCallAudioManager;
+ private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
private final TelecomSystem.SyncRoot mLock;
- Factory(CallAudioManager callAudioManager, TelecomSystem.SyncRoot lock) {
- mCallAudioManager = callAudioManager;
+ Factory(CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
+ TelecomSystem.SyncRoot lock) {
+ mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
mLock = lock;
}
- InCallTonePlayer createPlayer(int tone) {
- return new InCallTonePlayer(tone, mCallAudioManager, mLock);
+ public void setCallAudioManager(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
+ }
+
+ public InCallTonePlayer createPlayer(int tone) {
+ return new InCallTonePlayer(tone, mCallAudioManager,
+ mCallAudioRoutePeripheralAdapter, mLock);
}
}
@@ -83,6 +92,7 @@
private static int sTonesPlaying = 0;
private final CallAudioManager mCallAudioManager;
+ private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
@@ -103,10 +113,12 @@
private InCallTonePlayer(
int toneId,
CallAudioManager callAudioManager,
+ CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
TelecomSystem.SyncRoot lock) {
mState = STATE_OFF;
mToneId = toneId;
mCallAudioManager = callAudioManager;
+ mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
mLock = lock;
}
@@ -195,7 +207,7 @@
}
int stream = AudioManager.STREAM_VOICE_CALL;
- if (mCallAudioManager.isBluetoothAudioOn()) {
+ if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
stream = AudioManager.STREAM_BLUETOOTH_SCO;
}
@@ -236,8 +248,9 @@
cleanUpTonePlayer();
}
}
-
- void startTone() {
+
+ @VisibleForTesting
+ public void startTone() {
sTonesPlaying++;
if (sTonesPlaying == 1) {
mCallAudioManager.setIsTonePlaying(true);
@@ -249,7 +262,8 @@
/**
* Stops the tone.
*/
- void stopTone() {
+ @VisibleForTesting
+ public void stopTone() {
synchronized (this) {
if (mState == STATE_ON) {
Log.d(this, "Stopping the tone %d.", mToneId);
@@ -261,8 +275,9 @@
private void cleanUpTonePlayer() {
// Release focus on the main thread.
- mMainThreadHandler.post(new Runnable() {
- @Override public void run() {
+ mMainThreadHandler.post(new Runnable("ICTP.cUTP") {
+ @Override
+ public void loggedRun() {
synchronized (mLock) {
if (sTonesPlaying == 0) {
Log.wtf(this, "Over-releasing focus for tone player.");
@@ -271,6 +286,6 @@
}
}
}
- });
+ }.prepare());
}
}
diff --git a/src/com/android/server/telecom/InCallWakeLockController.java b/src/com/android/server/telecom/InCallWakeLockController.java
index a6c63c3..ffa6a3f 100644
--- a/src/com/android/server/telecom/InCallWakeLockController.java
+++ b/src/com/android/server/telecom/InCallWakeLockController.java
@@ -18,31 +18,21 @@
import com.android.internal.annotations.VisibleForTesting;
-import android.content.Context;
-import android.os.PowerManager;
-
/**
* Handles acquisition and release of wake locks relating to call state.
*/
@VisibleForTesting
public class InCallWakeLockController extends CallsManagerListenerBase {
- private static final String TAG = "InCallWakeLockContoller";
-
- private final Context mContext;
- private final PowerManager.WakeLock mFullWakeLock;
+ private final TelecomWakeLock mTelecomWakeLock;
private final CallsManager mCallsManager;
@VisibleForTesting
- public InCallWakeLockController(Context context, CallsManager callsManager) {
- mContext = context;
+ public InCallWakeLockController(TelecomWakeLock telecomWakeLock, CallsManager callsManager) {
mCallsManager = callsManager;
- PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
- mFullWakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG);
- mFullWakeLock.setReferenceCounted(false);
-
- callsManager.addListener(this);
+ mTelecomWakeLock = telecomWakeLock;
+ mTelecomWakeLock.setReferenceCounted(false);
}
@Override
@@ -64,10 +54,10 @@
// We grab a full lock as long as there exists a ringing call.
Call ringingCall = mCallsManager.getRingingCall();
if (ringingCall != null) {
- mFullWakeLock.acquire();
+ mTelecomWakeLock.acquire();
Log.i(this, "Acquiring full wake lock");
- } else if (mFullWakeLock.isHeld()) {
- mFullWakeLock.release();
+ } else {
+ mTelecomWakeLock.release(0);
Log.i(this, "Releasing full wake lock");
}
}
diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java
index f0ee3fe..5e2dc9d 100644
--- a/src/com/android/server/telecom/Log.java
+++ b/src/com/android/server/telecom/Log.java
@@ -16,25 +16,34 @@
package com.android.server.telecom;
+import android.content.Context;
import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.AsyncTask;
import android.telecom.PhoneAccount;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
+import android.util.Base64;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.IllegalFormatException;
+import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
/**
@@ -68,6 +77,7 @@
public static final String STOP_DTMF = "STOP_DTMF";
public static final String START_RINGER = "START_RINGER";
public static final String STOP_RINGER = "STOP_RINGER";
+ public static final String SKIP_RINGING = "SKIP_RINGING";
public static final String START_CALL_WAITING_TONE = "START_CALL_WAITING_TONE";
public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
public static final String START_CONNECTION = "START_CONNECTION";
@@ -82,6 +92,15 @@
public static final String MUTE = "MUTE";
public static final String AUDIO_ROUTE = "AUDIO_ROUTE";
public static final String ERROR_LOG = "ERROR";
+ public static final String USER_LOG_MARK = "USER_LOG_MARK";
+ public static final String SILENCE = "SILENCE";
+ public static final String BIND_SCREENING = "BIND_SCREENING";
+ public static final String SCREENING_BOUND = "SCREENING_BOUND";
+ public static final String SCREENING_SENT = "SCREENING_SENT";
+ public static final String SCREENING_TIMED_OUT = "SCREENING_TIMED_OUT";
+ public static final String BLOCK_CHECK_INITIATED = "BLOCK_CHECK_INITIATED";
+ public static final String BLOCK_CHECK_TIMED_OUT = "BLOCK_CHECK_TIMED_OUT";
+ public static final String BLOCK_CHECK_FINISHED = "BLOCK_CHECK_FINISHED";
/**
* Maps from a request to a response. The same event could be listed as the
@@ -98,16 +117,20 @@
put(REQUEST_UNHOLD, SET_ACTIVE);
put(START_CONNECTION, SET_DIALING);
put(BIND_CS, CS_BOUND);
+ put(SCREENING_SENT, SCREENING_TIMED_OUT);
+ put(BLOCK_CHECK_INITIATED, BLOCK_CHECK_TIMED_OUT);
}};
}
public static class CallEvent {
public String eventId;
+ public String sessionId;
public long time;
public Object data;
- public CallEvent(String eventId, long time, Object data) {
+ public CallEvent(String eventId, String sessionId, long time, Object data) {
this.eventId = eventId;
+ this.sessionId = sessionId;
this.time = time;
this.data = data;
}
@@ -118,27 +141,25 @@
private static int sNextId = 1;
private final List<CallEvent> mEvents = new LinkedList<>();
private final Call mCall;
- private final int mId;
public CallEventRecord(Call call) {
mCall = call;
- mId = ++sNextId;
}
public Call getCall() {
return mCall;
}
- public void addEvent(String event, Object data) {
- mEvents.add(new CallEvent(event, System.currentTimeMillis(), data));
- Log.i("Event", "Call %d: %s, %s", mId, event, data);
+ public void addEvent(String event, String sessionId, Object data) {
+ mEvents.add(new CallEvent(event, sessionId, System.currentTimeMillis(), data));
+ Log.i("Event", "Call %s: %s, %s", mCall.getId(), event, data);
}
public void dump(IndentingPrintWriter pw) {
Map<String, CallEvent> pendingResponses = new HashMap<>();
pw.print("Call ");
- pw.print(mId);
+ pw.print(mCall.getId());
pw.print(" [");
pw.print(sDateFormat.format(new Date(mCall.getCreationTimeMillis())));
pw.print("]");
@@ -174,7 +195,7 @@
// ID instead.
CallEventRecord record = mCallEventRecordMap.get(data);
if (record != null) {
- data = "Call " + record.mId;
+ data = "Call " + record.mCall.getId();
}
}
@@ -192,6 +213,8 @@
pw.print(event.time - requestEvent.time);
pw.print(" ms");
}
+ pw.print(":");
+ pw.print(event.sessionId);
pw.println();
}
pw.decreaseIndent();
@@ -199,10 +222,19 @@
}
public static final int MAX_CALLS_TO_CACHE = 5; // Arbitrarily chosen.
+ public static final int MAX_CALLS_TO_CACHE_DEBUG = 20; // Arbitrarily chosen.
+ private static final long EXTENDED_LOGGING_DURATION_MILLIS = 60000 * 30; // 30 minutes
+
+ // Don't check in with this true!
+ private static final boolean LOG_DBG = false;
+
+ // Currently using 3 letters, So don't exceed 64^3
+ private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144;
// Generic tag for all In Call logging
@VisibleForTesting
public static String TAG = "Telecom";
+ public static String LOGGING_TAG = "Logging";
public static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */
public static final boolean SYSTRACE_DEBUG = false; /* STOP SHIP if true */
@@ -213,43 +245,405 @@
public static final boolean ERROR = isLoggable(android.util.Log.ERROR);
private static final Map<Call, CallEventRecord> mCallEventRecordMap = new HashMap<>();
- private static final LinkedBlockingQueue<CallEventRecord> mCallEventRecords =
+ private static LinkedBlockingQueue<CallEventRecord> mCallEventRecords =
new LinkedBlockingQueue<CallEventRecord>(MAX_CALLS_TO_CACHE);
- private Log() {}
+ private static Context mContext = null;
+ // Synchronized in all method calls
+ private static int sCodeEntryCounter = 0;
+ @VisibleForTesting
+ public static ConcurrentHashMap<Integer, Session> sSessionMapper = new ConcurrentHashMap<>(100);
+ @VisibleForTesting
+ public static Handler sSessionCleanupHandler = new Handler(Looper.getMainLooper());
+ @VisibleForTesting
+ public static Runnable sCleanStaleSessions = new Runnable("L.cSS") {
+ @Override
+ public void loggedRun() {
+ cleanupStaleSessions(getSessionCleanupTimeoutMs());
+ }
+ };
+
+ // Set the logging container to be the system's. This will only change when being mocked
+ // during testing.
+ private static SystemLoggingContainer systemLogger = new SystemLoggingContainer();
+
+ /**
+ * Tracks whether user-activated extended logging is enabled.
+ */
+ private static boolean mIsUserExtendedLoggingEnabled = false;
+
+ /**
+ * The time when user-activated extended logging should be ended. Used to determine when
+ * extended logging should automatically be disabled.
+ */
+ private static long mUserExtendedLoggingStopTime = 0;
+
+ private Log() {
+ }
+
+ public static void setContext(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Enable or disable extended telecom logging.
+ *
+ * @param isExtendedLoggingEnabled {@code true} if extended logging should be enabled,
+ * {@code false} if it should be disabled.
+ */
+ public static void setIsExtendedLoggingEnabled(boolean isExtendedLoggingEnabled) {
+ // If the state hasn't changed, bail early.
+ if (mIsUserExtendedLoggingEnabled == isExtendedLoggingEnabled) {
+ return;
+ }
+
+ // Resize the event queue.
+ int newSize = isExtendedLoggingEnabled ? MAX_CALLS_TO_CACHE_DEBUG : MAX_CALLS_TO_CACHE;
+ LinkedBlockingQueue<CallEventRecord> oldEventLog = mCallEventRecords;
+ mCallEventRecords = new LinkedBlockingQueue<CallEventRecord>(newSize);
+ mCallEventRecordMap.clear();
+
+ // Copy the existing queue into the new one.
+ for (CallEventRecord event : oldEventLog) {
+ addCallEventRecord(event);
+ }
+
+ mIsUserExtendedLoggingEnabled = isExtendedLoggingEnabled;
+ if (mIsUserExtendedLoggingEnabled) {
+ mUserExtendedLoggingStopTime = System.currentTimeMillis()
+ + EXTENDED_LOGGING_DURATION_MILLIS;
+ } else {
+ mUserExtendedLoggingStopTime = 0;
+ }
+ }
+
+ public static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
+ private static MessageDigest sMessageDigest;
+
+ public static void initMd5Sum() {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... args) {
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ md = null;
+ }
+ sMessageDigest = md;
+ return null;
+ }
+ }.execute();
+ }
@VisibleForTesting
public static void setTag(String tag) {
TAG = tag;
}
+ @VisibleForTesting
+ public static void setLoggingContainer(SystemLoggingContainer logger) {
+ systemLogger = logger;
+ }
+
+ // Overridden in LogTest to skip query to ContentProvider
+ public interface ISessionCleanupTimeoutMs {
+ long get();
+ }
+ @VisibleForTesting
+ public static ISessionCleanupTimeoutMs sSessionCleanupTimeoutMs =
+ new ISessionCleanupTimeoutMs() {
+ @Override
+ public long get() {
+ // mContext will be null if Log is called from another process
+ // (UserCallActivity, for example). For these cases, use the default value.
+ if(mContext == null) {
+ return DEFAULT_SESSION_TIMEOUT_MS;
+ }
+ return Timeouts.getStaleSessionCleanupTimeoutMillis(
+ mContext.getContentResolver());
+ }
+ };
+
+ private static long getSessionCleanupTimeoutMs() {
+ return sSessionCleanupTimeoutMs.get();
+ }
+
+ private static synchronized void resetStaleSessionTimer() {
+ sSessionCleanupHandler.removeCallbacksAndMessages(null);
+ // Will be null in Log Testing
+ if (sCleanStaleSessions != null) {
+ sSessionCleanupHandler.postDelayed(sCleanStaleSessions.prepare(),
+ getSessionCleanupTimeoutMs());
+ }
+ }
+
+ /**
+ * Call at an entry point to the Telecom code to track the session. This code must be
+ * accompanied by a Log.endSession().
+ */
+ public static synchronized void startSession(String shortMethodName) {
+ startSession(shortMethodName, null);
+ }
+ public static synchronized void startSession(String shortMethodName,
+ String callerIdentification) {
+ resetStaleSessionTimer();
+ int threadId = getCallingThreadId();
+ Session activeSession = sSessionMapper.get(threadId);
+ // We have called startSession within an active session that has not ended... Register this
+ // session as a subsession.
+ if (activeSession != null) {
+ Session childSession = createSubsession(true);
+ continueSession(childSession, shortMethodName);
+ return;
+ }
+ Session newSession = new Session(getNextSessionID(), shortMethodName,
+ System.currentTimeMillis(), threadId, false, callerIdentification);
+ sSessionMapper.put(threadId, newSession);
+
+ Log.v(LOGGING_TAG, Session.START_SESSION);
+ }
+
+
+ /**
+ * Notifies the logging system that a subsession will be run at a later point and
+ * allocates the resources. Returns a session object that must be used in
+ * Log.continueSession(...) to start the subsession.
+ */
+ public static Session createSubsession() {
+ return createSubsession(false);
+ }
+
+ private static synchronized Session createSubsession(boolean isStartedFromActiveSession) {
+ int threadId = getCallingThreadId();
+ Session threadSession = sSessionMapper.get(threadId);
+ if (threadSession == null) {
+ Log.d(LOGGING_TAG, "Log.createSubsession was called with no session active.");
+ return null;
+ }
+ // Start execution time of the session will be overwritten in continueSession(...).
+ Session newSubsession = new Session(threadSession.getNextChildId(),
+ threadSession.getShortMethodName(), System.currentTimeMillis(), threadId,
+ isStartedFromActiveSession, null);
+ threadSession.addChild(newSubsession);
+ newSubsession.setParentSession(threadSession);
+
+ if(!isStartedFromActiveSession) {
+ Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " + newSubsession.toString());
+ } else {
+ Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " (Invisible subsession)");
+ }
+ return newSubsession;
+ }
+
+ /**
+ * Cancels a subsession that had Log.createSubsession() called on it, but will never have
+ * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned
+ * gracefully instead of being removed by the sSessionCleanupHandler forcefully later.
+ */
+ public static synchronized void cancelSubsession(Session subsession) {
+ if (subsession == null) {
+ return;
+ }
+
+ subsession.markSessionCompleted(0);
+ endParentSessions(subsession);
+ }
+
+ /**
+ * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method
+ * must be called at the end of this method. The full session will complete when all
+ * subsessions are completed.
+ */
+ public static synchronized void continueSession(Session subsession, String shortMethodName) {
+ if (subsession == null) {
+ return;
+ }
+ resetStaleSessionTimer();
+ String callingMethodName = subsession.getShortMethodName();
+ subsession.setShortMethodName(callingMethodName + "->" + shortMethodName);
+ subsession.setExecutionStartTimeMs(System.currentTimeMillis());
+ Session parentSession = subsession.getParentSession();
+ if (parentSession == null) {
+ Log.d(LOGGING_TAG, "Log.continueSession was called with no session active for " +
+ "method %s.", shortMethodName);
+ return;
+ }
+
+ sSessionMapper.put(getCallingThreadId(), subsession);
+ if(!subsession.isStartedFromActiveSession()) {
+ Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
+ } else {
+ Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION + " (Invisible Subsession) with " +
+ "Method " + shortMethodName);
+ }
+ }
+
+ public static void checkIsThreadLogged() {
+ int threadId = getCallingThreadId();
+ Session threadSession = sSessionMapper.get(threadId);
+ if (threadSession == null) {
+ android.util.Log.e(LOGGING_TAG, "Logging Thread Check Failed!", new Exception());
+ }
+ }
+
+ /**
+ * Ends the current session/subsession. Must be called after a Log.startSession(...) and
+ * Log.continueSession(...) call.
+ */
+ public static synchronized void endSession() {
+ int threadId = getCallingThreadId();
+ Session completedSession = sSessionMapper.get(threadId);
+ if (completedSession == null) {
+ Log.w(LOGGING_TAG, "Log.endSession was called with no session active.");
+ return;
+ }
+
+ completedSession.markSessionCompleted(System.currentTimeMillis());
+ if(!completedSession.isStartedFromActiveSession()) {
+ Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
+ completedSession.getLocalExecutionTime() + " mS)");
+ } else {
+ Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (Invisible Subsession) (dur: " +
+ completedSession.getLocalExecutionTime() + " mS)");
+ }
+ // Remove after completed so that reference still exists for logging the end events
+ Session parentSession = completedSession.getParentSession();
+ sSessionMapper.remove(threadId);
+ endParentSessions(completedSession);
+ // If this subsession was started from a parent session using Log.startSession, return the
+ // ThreadID back to the parent after completion.
+ if (parentSession != null && !parentSession.isSessionCompleted() &&
+ completedSession.isStartedFromActiveSession()) {
+ sSessionMapper.put(threadId, parentSession);
+ }
+ }
+
+ // Recursively deletes all complete parent sessions of the current subsession if it is a leaf.
+ private static void endParentSessions(Session subsession) {
+ // Session is not completed or not currently a leaf, so we can not remove because a child is
+ // still running
+ if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
+ return;
+ }
+
+ Session parentSession = subsession.getParentSession();
+ if (parentSession != null) {
+ subsession.setParentSession(null);
+ parentSession.removeChild(subsession);
+ endParentSessions(parentSession);
+ } else {
+ // All of the subsessions have been completed and it is time to report on the full
+ // running time of the session.
+ long fullSessionTimeMs =
+ System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
+ Log.v(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs + " ms): " +
+ subsession.toString());
+ }
+ }
+
+ private synchronized static String getNextSessionID() {
+ Integer nextId = sCodeEntryCounter++;
+ if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) {
+ restartSessionCounter();
+ nextId = sCodeEntryCounter++;
+ }
+ return getBase64Encoding(nextId);
+ }
+
+ @VisibleForTesting
+ public synchronized static void restartSessionCounter() {
+ sCodeEntryCounter = 0;
+ }
+
+ @VisibleForTesting
+ public static String getBase64Encoding(int number) {
+ byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
+ idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
+ return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
+ }
+
+ public static int getCallingThreadId() {
+ return android.os.Process.myTid();
+ }
+
public static void event(Call call, String event) {
event(call, event, null);
}
public static void event(Call call, String event, Object data) {
+ Session currentSession = sSessionMapper.get(getCallingThreadId());
+ String currentSessionID = currentSession != null ? currentSession.toString() : "";
+
if (call == null) {
Log.i(TAG, "Non-call EVENT: %s, %s", event, data);
return;
}
synchronized (mCallEventRecords) {
if (!mCallEventRecordMap.containsKey(call)) {
- // First remove the oldest entry if no new ones exist.
- if (mCallEventRecords.remainingCapacity() == 0) {
- CallEventRecord record = mCallEventRecords.poll();
- if (record != null) {
- mCallEventRecordMap.remove(record.getCall());
- }
- }
-
- // Now add a new entry
CallEventRecord newRecord = new CallEventRecord(call);
- mCallEventRecords.add(newRecord);
- mCallEventRecordMap.put(call, newRecord);
+ addCallEventRecord(newRecord);
}
CallEventRecord record = mCallEventRecordMap.get(call);
- record.addEvent(event, data);
+ record.addEvent(event, currentSessionID, data);
+ }
+ }
+
+ @VisibleForTesting
+ public static void cleanupStaleSessions(long timeoutMs) {
+ String logMessage = "Stale Sessions Cleaned:\n";
+ boolean isSessionsStale = false;
+ long currentTimeMs = System.currentTimeMillis();
+ // Remove references that are in the Session Mapper (causing GC to occur) on
+ // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS.
+ // If this occurs, then there is most likely a Session active that never had
+ // Log.endSession called on it.
+ for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it =
+ sSessionMapper.entrySet().iterator(); it.hasNext(); ) {
+ ConcurrentHashMap.Entry<Integer, Session> entry = it.next();
+ Session session = entry.getValue();
+ if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) {
+ it.remove();
+ logMessage += session.printFullSessionTree() + "\n";
+ isSessionsStale = true;
+ }
+ }
+ if (isSessionsStale) {
+ Log.w(LOGGING_TAG, logMessage);
+ } else {
+ Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
+ }
+ }
+
+ private static void addCallEventRecord(CallEventRecord newRecord) {
+ Call call = newRecord.getCall();
+
+ // First remove the oldest entry if no new ones exist.
+ if (mCallEventRecords.remainingCapacity() == 0) {
+ CallEventRecord record = mCallEventRecords.poll();
+ if (record != null) {
+ mCallEventRecordMap.remove(record.getCall());
+ }
+ }
+
+ // Now add a new entry
+ mCallEventRecords.add(newRecord);
+ mCallEventRecordMap.put(call, newRecord);
+ }
+
+ /**
+ * If user enabled extended logging is enabled and the time limit has passed, disables the
+ * extended logging.
+ */
+ private static void maybeDisableLogging() {
+ if (!mIsUserExtendedLoggingEnabled) {
+ return;
+ }
+
+ if (mUserExtendedLoggingStopTime < System.currentTimeMillis()) {
+ mUserExtendedLoggingStopTime = 0;
+ mIsUserExtendedLoggingEnabled = false;
}
}
@@ -258,83 +652,95 @@
}
public static void d(String prefix, String format, Object... args) {
- if (DEBUG) {
- android.util.Slog.d(TAG, buildMessage(prefix, format, args));
+ if (mIsUserExtendedLoggingEnabled) {
+ maybeDisableLogging();
+ systemLogger.i(TAG, buildMessage(prefix, format, args));
+ } else if (DEBUG) {
+ systemLogger.d(TAG, buildMessage(prefix, format, args));
}
}
public static void d(Object objectPrefix, String format, Object... args) {
- if (DEBUG) {
- android.util.Slog.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ if (mIsUserExtendedLoggingEnabled) {
+ maybeDisableLogging();
+ systemLogger.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ } else if (DEBUG) {
+ systemLogger.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
}
}
public static void i(String prefix, String format, Object... args) {
if (INFO) {
- android.util.Slog.i(TAG, buildMessage(prefix, format, args));
+ systemLogger.i(TAG, buildMessage(prefix, format, args));
}
}
public static void i(Object objectPrefix, String format, Object... args) {
if (INFO) {
- android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ systemLogger.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
}
}
public static void v(String prefix, String format, Object... args) {
- if (VERBOSE) {
- android.util.Slog.v(TAG, buildMessage(prefix, format, args));
+ if (mIsUserExtendedLoggingEnabled) {
+ maybeDisableLogging();
+ systemLogger.i(TAG, buildMessage(prefix, format, args));
+ } else if (VERBOSE) {
+ systemLogger.v(TAG, buildMessage(prefix, format, args));
}
}
public static void v(Object objectPrefix, String format, Object... args) {
- if (VERBOSE) {
- android.util.Slog.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ if (mIsUserExtendedLoggingEnabled) {
+ maybeDisableLogging();
+ systemLogger.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ } else if (VERBOSE) {
+ systemLogger.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
}
}
public static void w(String prefix, String format, Object... args) {
if (WARN) {
- android.util.Slog.w(TAG, buildMessage(prefix, format, args));
+ systemLogger.w(TAG, buildMessage(prefix, format, args));
}
}
public static void w(Object objectPrefix, String format, Object... args) {
if (WARN) {
- android.util.Slog.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+ systemLogger.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
}
}
public static void e(String prefix, Throwable tr, String format, Object... args) {
if (ERROR) {
- android.util.Slog.e(TAG, buildMessage(prefix, format, args), tr);
+ systemLogger.e(TAG, buildMessage(prefix, format, args), tr);
}
}
public static void e(Object objectPrefix, Throwable tr, String format, Object... args) {
if (ERROR) {
- android.util.Slog.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
+ systemLogger.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
tr);
}
}
public static void wtf(String prefix, Throwable tr, String format, Object... args) {
- android.util.Slog.wtf(TAG, buildMessage(prefix, format, args), tr);
+ systemLogger.wtf(TAG, buildMessage(prefix, format, args), tr);
}
public static void wtf(Object objectPrefix, Throwable tr, String format, Object... args) {
- android.util.Slog.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
+ systemLogger.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
tr);
}
public static void wtf(String prefix, String format, Object... args) {
String msg = buildMessage(prefix, format, args);
- android.util.Slog.wtf(TAG, msg, new IllegalStateException(msg));
+ systemLogger.wtf(TAG, msg, new IllegalStateException(msg));
}
public static void wtf(Object objectPrefix, String format, Object... args) {
String msg = buildMessage(getPrefixFromObject(objectPrefix), format, args);
- android.util.Slog.wtf(TAG, msg, new IllegalStateException(msg));
+ systemLogger.wtf(TAG, msg, new IllegalStateException(msg));
}
public static String piiHandle(Object pii) {
@@ -395,15 +801,14 @@
}
private static String secureHash(byte[] input) {
- MessageDigest messageDigest;
- try {
- messageDigest = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- return null;
+ if (sMessageDigest != null) {
+ sMessageDigest.reset();
+ sMessageDigest.update(input);
+ byte[] result = sMessageDigest.digest();
+ return encodeHex(result);
+ } else {
+ return "Uninitialized SHA1";
}
- messageDigest.update(input);
- byte[] result = messageDigest.digest();
- return encodeHex(result);
}
private static String encodeHex(byte[] bytes) {
@@ -425,6 +830,16 @@
}
private static String buildMessage(String prefix, String format, Object... args) {
+ if (LOG_DBG) {
+ checkIsThreadLogged();
+ }
+ // Incorporate thread ID and calling method into prefix
+ String sessionPostfix = "";
+ Session currentSession = sSessionMapper.get(getCallingThreadId());
+ if (currentSession != null) {
+ sessionPostfix = ": " + currentSession.toString();
+ }
+
String msg;
try {
msg = (args == null || args.length == 0) ? format
@@ -434,6 +849,6 @@
args.length);
msg = format + " (An error occurred while formatting the message.)";
}
- return String.format(Locale.US, "%s: %s", prefix, msg);
+ return String.format(Locale.US, "%s: %s%s", prefix, msg, sessionPostfix);
}
}
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
index 5a88c34..1125a8e 100644
--- a/src/com/android/server/telecom/MissedCallNotifier.java
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -16,18 +16,23 @@
package com.android.server.telecom;
+import android.os.UserHandle;
+
/**
* Creates a notification for calls that the user missed (neither answered nor rejected).
*/
public interface MissedCallNotifier extends CallsManager.CallsManagerListener {
- void clearMissedCalls();
+ void clearMissedCalls(UserHandle userHandle);
void showMissedCallNotification(Call call);
- void updateOnStartup(
+ void reloadFromDatabase(
TelecomSystem.SyncRoot lock,
CallsManager callsManager,
ContactsAsyncHelper contactsAsyncHelper,
- CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory);
+ CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
+ UserHandle userHandle);
+
+ void setCurrentUserHandle(UserHandle userHandle);
}
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 17ccdb1..6e8492e 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -31,9 +31,10 @@
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.DisconnectCause;
-import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
// TODO: Needed for move to system service: import com.android.internal.R;
/**
@@ -52,7 +53,8 @@
* Calls to emergency numbers are still broadcast for informative purposes. The call is placed
* prior to sending ACTION_NEW_OUTGOING_CALL and cannot be redirected nor prevented.
*/
-class NewOutgoingCallIntentBroadcaster {
+@VisibleForTesting
+public class NewOutgoingCallIntentBroadcaster {
private static final String EXTRA_ACTUAL_NUMBER_TO_DIAL =
"android.telecom.extra.ACTUAL_NUMBER_TO_DIAL";
@@ -72,6 +74,7 @@
private final Call mCall;
private final Intent mIntent;
private final Context mContext;
+ private final PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
/*
* Whether or not the outgoing call intent originated from the default phone application. If
@@ -79,12 +82,15 @@
*/
private final boolean mIsDefaultOrSystemPhoneApp;
- NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager, Call call,
- Intent intent, boolean isDefaultPhoneApp) {
+ @VisibleForTesting
+ public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager, Call call,
+ Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
+ boolean isDefaultPhoneApp) {
mContext = context;
mCallsManager = callsManager;
mCall = call;
mIntent = intent;
+ mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
}
@@ -92,57 +98,67 @@
* Processes the result of the outgoing call broadcast intent, and performs callbacks to
* the OutgoingCallIntentBroadcasterListener as necessary.
*/
- private class NewOutgoingCallBroadcastIntentReceiver extends BroadcastReceiver {
+ public class NewOutgoingCallBroadcastIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- Trace.beginSection("onReceiveNewOutgoingCallBroadcast");
- Log.v(this, "onReceive: %s", intent);
+ try {
+ Log.startSession("NOCBIR.oR");
+ Trace.beginSection("onReceiveNewOutgoingCallBroadcast");
+ Log.v(this, "onReceive: %s", intent);
- // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is used as the
- // actual number to call. (If null, no call will be placed.)
- String resultNumber = getResultData();
- Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
- Log.pii(resultNumber));
+ // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is used as the
+ // actual number to call. (If null, no call will be placed.)
+ String resultNumber = getResultData();
+ Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
+ Log.pii(resultNumber));
- boolean endEarly = false;
- if (resultNumber == null) {
- Log.v(this, "Call cancelled (null number), returning...");
- endEarly = true;
- } else if (PhoneNumberUtils.isPotentialLocalEmergencyNumber(mContext, resultNumber)) {
- Log.w(this, "Cannot modify outgoing call to emergency number %s.", resultNumber);
- endEarly = true;
- }
-
- if (endEarly) {
- if (mCall != null) {
- mCall.disconnect(true /* wasViaNewOutgoingCall */);
+ boolean endEarly = false;
+ if (resultNumber == null) {
+ Log.v(this, "Call cancelled (null number), returning...");
+ endEarly = true;
+ } else if (mPhoneNumberUtilsAdapter.isPotentialLocalEmergencyNumber(
+ mContext, resultNumber)) {
+ Log.w(this, "Cannot modify outgoing call to emergency number %s.",
+ resultNumber);
+ endEarly = true;
}
+
+ if (endEarly) {
+ if (mCall != null) {
+ mCall.disconnect(true /* wasViaNewOutgoingCall */);
+ }
+ return;
+ }
+
+ Uri resultHandleUri = Uri.fromParts(
+ mPhoneNumberUtilsAdapter.isUriNumber(resultNumber) ?
+ PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL,
+ resultNumber, null);
+
+ Uri originalUri = mIntent.getData();
+
+ if (originalUri.getSchemeSpecificPart().equals(resultNumber)) {
+ Log.v(this, "Call number unmodified after new outgoing call intent broadcast.");
+ } else {
+ Log.v(this, "Retrieved modified handle after outgoing call intent broadcast: "
+ + "Original: %s, Modified: %s",
+ Log.pii(originalUri),
+ Log.pii(resultHandleUri));
+ }
+
+ GatewayInfo gatewayInfo = getGateWayInfoFromIntent(intent, resultHandleUri);
+ mCall.setNewOutgoingCallIntentBroadcastIsDone();
+ mCallsManager.placeOutgoingCall(mCall, resultHandleUri, gatewayInfo,
+ mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
+ false),
+ mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ VideoProfile.STATE_AUDIO_ONLY));
+
+ } finally {
Trace.endSection();
- return;
+ Log.endSession();
}
-
- Uri resultHandleUri = Uri.fromParts(PhoneNumberUtils.isUriNumber(resultNumber) ?
- PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, resultNumber, null);
-
- Uri originalUri = mIntent.getData();
-
- if (originalUri.getSchemeSpecificPart().equals(resultNumber)) {
- Log.v(this, "Call number unmodified after new outgoing call intent broadcast.");
- } else {
- Log.v(this, "Retrieved modified handle after outgoing call intent broadcast: "
- + "Original: %s, Modified: %s",
- Log.pii(originalUri),
- Log.pii(resultHandleUri));
- }
-
- GatewayInfo gatewayInfo = getGateWayInfoFromIntent(intent, resultHandleUri);
- mCallsManager.placeOutgoingCall(mCall, resultHandleUri, gatewayInfo,
- mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
- false),
- mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
- VideoProfile.STATE_AUDIO_ONLY));
- Trace.endSection();
}
}
@@ -159,7 +175,8 @@
* @return {@link DisconnectCause#NOT_DISCONNECTED} if the call succeeded, and an appropriate
* {@link DisconnectCause} if the call did not, describing why it failed.
*/
- int processIntent() {
+ @VisibleForTesting
+ public int processIntent() {
Log.v(this, "Processing call intent in OutgoingCallIntentBroadcaster.");
Intent intent = mIntent;
@@ -191,16 +208,16 @@
}
}
- String number = PhoneNumberUtils.getNumberFromIntent(intent, mContext);
+ String number = mPhoneNumberUtilsAdapter.getNumberFromIntent(intent, mContext);
if (TextUtils.isEmpty(number)) {
Log.w(this, "Empty number obtained from the call intent.");
return DisconnectCause.NO_PHONE_NUMBER_SUPPLIED;
}
- boolean isUriNumber = PhoneNumberUtils.isUriNumber(number);
+ boolean isUriNumber = mPhoneNumberUtilsAdapter.isUriNumber(number);
if (!isUriNumber) {
- number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
- number = PhoneNumberUtils.stripSeparators(number);
+ number = mPhoneNumberUtilsAdapter.convertKeypadLettersToDigits(number);
+ number = mPhoneNumberUtilsAdapter.stripSeparators(number);
}
final boolean isPotentialEmergencyNumber = isPotentialEmergencyNumber(number);
@@ -253,8 +270,9 @@
// initiate the call again because of the presence of the EXTRA_ALREADY_CALLED extra.
}
- Log.i(this, "Sending NewOutgoingCallBroadcast for %s", mCall);
- broadcastIntent(intent, number, !callImmediately);
+ UserHandle targetUser = mCall.getInitiatingUser();
+ Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
+ broadcastIntent(intent, number, !callImmediately, targetUser);
return DisconnectCause.NOT_DISCONNECTED;
}
@@ -265,12 +283,14 @@
* @param originalCallIntent The original call intent.
* @param number Call number that was stored in the original call intent.
* @param receiverRequired Whether or not the result from the ordered broadcast should be
- * processed using a {@link NewOutgoingCallIntentBroadcaster}.
+ * processed using a {@link NewOutgoingCallIntentBroadcaster}.
+ * @param targetUser User that the broadcast sent to.
*/
private void broadcastIntent(
Intent originalCallIntent,
String number,
- boolean receiverRequired) {
+ boolean receiverRequired,
+ UserHandle targetUser) {
Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
if (number != null) {
broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
@@ -285,7 +305,7 @@
mContext.sendOrderedBroadcastAsUser(
broadcastIntent,
- UserHandle.CURRENT,
+ targetUser,
android.Manifest.permission.PROCESS_OUTGOING_CALLS,
AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
@@ -387,8 +407,8 @@
*/
private boolean isPotentialEmergencyNumber(String number) {
Log.v(this, "Checking restrictions for number : %s", Log.pii(number));
- return (number != null) && PhoneNumberUtils.isPotentialLocalEmergencyNumber(mContext,
- number);
+ return (number != null)
+ && mPhoneNumberUtilsAdapter.isPotentialLocalEmergencyNumber(mContext, number);
}
/**
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
new file mode 100644
index 0000000..ae8e425
--- /dev/null
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Connection;
+import android.telecom.ParcelableCall;
+import android.telecom.TelecomManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utilities dealing with {@link ParcelableCall}.
+ */
+public class ParcelableCallUtils {
+ /**
+ * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
+ *
+ * @param call The {@link Call} to parcel.
+ * @param includeVideoProvider {@code true} if the video provider should be parcelled with the
+ * {@link Call}, {@code false} otherwise. Since the {@link ParcelableCall#getVideoCall()}
+ * method creates a {@link VideoCallImpl} instance on access it is important for the
+ * recipient of the {@link ParcelableCall} to know if the video provider changed.
+ * @param phoneAccountRegistrar The {@link PhoneAccountRegistrar}.
+ * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
+ */
+ public static ParcelableCall toParcelableCall(
+ Call call,
+ boolean includeVideoProvider,
+ PhoneAccountRegistrar phoneAccountRegistrar) {
+ int state = getParcelableState(call);
+ int capabilities = convertConnectionToCallCapabilities(call.getConnectionCapabilities());
+ int properties = convertConnectionToCallProperties(call.getConnectionCapabilities());
+ if (call.isConference()) {
+ properties |= android.telecom.Call.Details.PROPERTY_CONFERENCE;
+ }
+
+ if (call.isWorkCall()) {
+ properties |= android.telecom.Call.Details.PROPERTY_WORK_CALL;
+ }
+
+ // If this is a single-SIM device, the "default SIM" will always be the only SIM.
+ boolean isDefaultSmsAccount =
+ phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
+ if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) {
+ capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT;
+ }
+
+ if (call.isEmergencyCall()) {
+ capabilities = removeCapability(
+ capabilities, android.telecom.Call.Details.CAPABILITY_MUTE);
+ }
+
+ if (state == android.telecom.Call.STATE_DIALING) {
+ capabilities = removeCapability(capabilities,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
+ capabilities = removeCapability(capabilities,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
+ }
+
+ String parentCallId = null;
+ Call parentCall = call.getParentCall();
+ if (parentCall != null) {
+ parentCallId = parentCall.getId();
+ }
+
+ long connectTimeMillis = call.getConnectTimeMillis();
+ List<Call> childCalls = call.getChildCalls();
+ List<String> childCallIds = new ArrayList<>();
+ if (!childCalls.isEmpty()) {
+ long childConnectTimeMillis = Long.MAX_VALUE;
+ for (Call child : childCalls) {
+ if (child.getConnectTimeMillis() > 0) {
+ childConnectTimeMillis = Math.min(child.getConnectTimeMillis(),
+ childConnectTimeMillis);
+ }
+ childCallIds.add(child.getId());
+ }
+
+ if (childConnectTimeMillis != Long.MAX_VALUE) {
+ connectTimeMillis = childConnectTimeMillis;
+ }
+ }
+
+ Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
+ call.getHandle() : null;
+ String callerDisplayName = call.getCallerDisplayNamePresentation() ==
+ TelecomManager.PRESENTATION_ALLOWED ? call.getCallerDisplayName() : null;
+
+ List<Call> conferenceableCalls = call.getConferenceableCalls();
+ List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
+ for (Call otherCall : conferenceableCalls) {
+ conferenceableCallIds.add(otherCall.getId());
+ }
+
+ return new ParcelableCall(
+ call.getId(),
+ state,
+ call.getDisconnectCause(),
+ call.getCannedSmsResponses(),
+ capabilities,
+ properties,
+ connectTimeMillis,
+ handle,
+ call.getHandlePresentation(),
+ callerDisplayName,
+ call.getCallerDisplayNamePresentation(),
+ call.getGatewayInfo(),
+ call.getTargetPhoneAccount(),
+ includeVideoProvider,
+ includeVideoProvider ? call.getVideoProvider() : null,
+ parentCallId,
+ childCallIds,
+ call.getStatusHints(),
+ call.getVideoState(),
+ conferenceableCallIds,
+ call.getIntentExtras(),
+ call.getExtras());
+ }
+
+ private static int getParcelableState(Call call) {
+ int state = CallState.NEW;
+ switch (call.getState()) {
+ case CallState.ABORTED:
+ case CallState.DISCONNECTED:
+ state = android.telecom.Call.STATE_DISCONNECTED;
+ break;
+ case CallState.ACTIVE:
+ state = android.telecom.Call.STATE_ACTIVE;
+ break;
+ case CallState.CONNECTING:
+ state = android.telecom.Call.STATE_CONNECTING;
+ break;
+ case CallState.DIALING:
+ state = android.telecom.Call.STATE_DIALING;
+ break;
+ case CallState.DISCONNECTING:
+ state = android.telecom.Call.STATE_DISCONNECTING;
+ break;
+ case CallState.NEW:
+ state = android.telecom.Call.STATE_NEW;
+ break;
+ case CallState.ON_HOLD:
+ state = android.telecom.Call.STATE_HOLDING;
+ break;
+ case CallState.RINGING:
+ state = android.telecom.Call.STATE_RINGING;
+ break;
+ case CallState.SELECT_PHONE_ACCOUNT:
+ state = android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT;
+ break;
+ }
+
+ // If we are marked as 'locally disconnecting' then mark ourselves as disconnecting instead.
+ // Unless we're disconnect*ED*, in which case leave it at that.
+ if (call.isLocallyDisconnecting() &&
+ (state != android.telecom.Call.STATE_DISCONNECTED)) {
+ state = android.telecom.Call.STATE_DISCONNECTING;
+ }
+ return state;
+ }
+
+ private static final int[] CONNECTION_TO_CALL_CAPABILITY = new int[] {
+ Connection.CAPABILITY_HOLD,
+ android.telecom.Call.Details.CAPABILITY_HOLD,
+
+ Connection.CAPABILITY_SUPPORT_HOLD,
+ android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD,
+
+ Connection.CAPABILITY_MERGE_CONFERENCE,
+ android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE,
+
+ Connection.CAPABILITY_SWAP_CONFERENCE,
+ android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE,
+
+ Connection.CAPABILITY_RESPOND_VIA_TEXT,
+ android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT,
+
+ Connection.CAPABILITY_MUTE,
+ android.telecom.Call.Details.CAPABILITY_MUTE,
+
+ Connection.CAPABILITY_MANAGE_CONFERENCE,
+ android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE,
+
+ Connection.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
+
+ Connection.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
+
+ Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
+
+ Connection.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
+
+ Connection.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
+
+ Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
+ android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
+
+ Connection.CAPABILITY_SEPARATE_FROM_CONFERENCE,
+ android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE,
+
+ Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
+ android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
+
+ Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
+ android.telecom.Call.Details.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
+
+ Connection.CAPABILITY_CAN_PAUSE_VIDEO,
+ android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO,
+
+ Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION,
+ android.telecom.Call.Details.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION,
+
+ Connection.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO,
+ android.telecom.Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO
+ };
+
+ private static int convertConnectionToCallCapabilities(int connectionCapabilities) {
+ int callCapabilities = 0;
+ for (int i = 0; i < CONNECTION_TO_CALL_CAPABILITY.length; i += 2) {
+ if ((CONNECTION_TO_CALL_CAPABILITY[i] & connectionCapabilities) ==
+ CONNECTION_TO_CALL_CAPABILITY[i]) {
+
+ callCapabilities |= CONNECTION_TO_CALL_CAPABILITY[i + 1];
+ }
+ }
+ return callCapabilities;
+ }
+
+ private static final int[] CONNECTION_TO_CALL_PROPERTIES = new int[] {
+ Connection.CAPABILITY_HIGH_DEF_AUDIO,
+ android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO,
+
+ Connection.CAPABILITY_WIFI,
+ android.telecom.Call.Details.PROPERTY_WIFI,
+
+ Connection.CAPABILITY_GENERIC_CONFERENCE,
+ android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE,
+
+ Connection.CAPABILITY_SHOW_CALLBACK_NUMBER,
+ android.telecom.Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE
+ };
+
+ private static int convertConnectionToCallProperties(int connectionCapabilities) {
+ int callProperties = 0;
+ for (int i = 0; i < CONNECTION_TO_CALL_PROPERTIES.length; i += 2) {
+ if ((CONNECTION_TO_CALL_PROPERTIES[i] & connectionCapabilities) ==
+ CONNECTION_TO_CALL_PROPERTIES[i]) {
+
+ callProperties |= CONNECTION_TO_CALL_PROPERTIES[i + 1];
+ }
+ }
+ return callProperties;
+ }
+
+ /**
+ * Removes the specified capability from the set of capabilities bits and returns the new set.
+ */
+ private static int removeCapability(int capabilities, int capability) {
+ return capabilities & ~capability;
+ }
+
+ private ParcelableCallUtils() {}
+}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 28ede77..19c1a01 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -16,7 +16,6 @@
package com.android.server.telecom;
-import android.app.ActivityManager;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
@@ -29,7 +28,8 @@
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;
import android.os.Process;
import android.os.UserHandle;
@@ -74,7 +74,9 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -95,20 +97,16 @@
* 3) The user running the app that is requesting the phone account information.
*
* For example, I have a device with 2 users, primary (A) and secondary (B), and the secondary user
- * has a work profile running as another user (B2). Lets say that user B opens the phone settings
- * (not currently supported, but theoretically speaking), and phone settings queries for a phone
- * account list. Lets also say that an app running in the work profile has registered a phone
- * account. This means that:
- *
- * Since phone settings always runs as the primary user, We have the following situation:
- * User A (settings) is requesting a list of phone accounts while the active user is User B, and
- * that list contains a phone account for profile User B2.
+ * has a work profile running as another user (B2). Each user/profile only have the visibility of
+ * phone accounts owned by them. Lets say, user B (settings) is requesting a list of phone accounts,
+ * and the list only contains phone accounts owned by user B and accounts with
+ * {@link PhoneAccount#CAPABILITY_MULTI_USER}.
*
* In practice, (2) is stored with the phone account handle and is part of the handle's ID. (1) is
* saved in {@link #mCurrentUserHandle} and (3) we get from Binder.getCallingUser(). We check these
* users for visibility before returning any phone accounts.
*/
-public final class PhoneAccountRegistrar {
+public class PhoneAccountRegistrar {
public static final PhoneAccountHandle NO_ACCOUNT_SELECTED =
new PhoneAccountHandle(new ComponentName("null", "null"), "NO_ACCOUNT_SELECTED");
@@ -121,7 +119,7 @@
private static final String FILE_NAME = "phone-account-registrar-state.xml";
@VisibleForTesting
- public static final int EXPECTED_STATE_VERSION = 8;
+ public static final int EXPECTED_STATE_VERSION = 9;
/** Keep in sync with the same in SipSettings.java */
private static final String SIP_SHARED_PREFERENCES = "SIP_PREFERENCES";
@@ -133,6 +131,9 @@
private final SubscriptionManager mSubscriptionManager;
private State mState;
private UserHandle mCurrentUserHandle;
+ private interface PhoneAccountRegistrarWriteLock {}
+ private final PhoneAccountRegistrarWriteLock mWriteLock =
+ new PhoneAccountRegistrarWriteLock() {};
@VisibleForTesting
public PhoneAccountRegistrar(Context context) {
@@ -168,7 +169,7 @@
* @return The value of the subscription id or -1 if it does not exist or is not valid.
*/
public int getSubscriptionIdForPhoneAccount(PhoneAccountHandle accountHandle) {
- PhoneAccount account = getPhoneAccountCheckCallingUser(accountHandle);
+ PhoneAccount account = getPhoneAccountUnchecked(accountHandle);
if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
TelephonyManager tm =
@@ -186,19 +187,21 @@
* @param uriScheme The URI scheme for the outgoing call.
* @return The {@link PhoneAccountHandle} to use.
*/
- public PhoneAccountHandle getOutgoingPhoneAccountForScheme(String uriScheme) {
- final PhoneAccountHandle userSelected = getUserSelectedOutgoingPhoneAccount();
+ public PhoneAccountHandle getOutgoingPhoneAccountForScheme(String uriScheme,
+ UserHandle userHandle) {
+ final PhoneAccountHandle userSelected = getUserSelectedOutgoingPhoneAccount(userHandle);
if (userSelected != null) {
// If there is a default PhoneAccount, ensure it supports calls to handles with the
// specified uriScheme.
- final PhoneAccount userSelectedAccount = getPhoneAccountCheckCallingUser(userSelected);
+ final PhoneAccount userSelectedAccount = getPhoneAccountUnchecked(userSelected);
if (userSelectedAccount.supportsUriScheme(uriScheme)) {
return userSelected;
}
}
- List<PhoneAccountHandle> outgoing = getCallCapablePhoneAccounts(uriScheme, false);
+ List<PhoneAccountHandle> outgoing = getCallCapablePhoneAccounts(uriScheme, false,
+ userHandle);
switch (outgoing.size()) {
case 0:
// There are no accounts, so there can be no default
@@ -212,14 +215,30 @@
}
}
+ public PhoneAccountHandle getOutgoingPhoneAccountForSchemeOfCurrentUser(String uriScheme) {
+ return getOutgoingPhoneAccountForScheme(uriScheme, mCurrentUserHandle);
+ }
+
/**
* @return The user-selected outgoing {@link PhoneAccount}, or null if it hasn't been set (or
* if it was set by another user).
*/
- PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
- PhoneAccount account = getPhoneAccountCheckCallingUser(mState.defaultOutgoing);
+ @VisibleForTesting
+ public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount(UserHandle userHandle) {
+ if (userHandle == null) {
+ return null;
+ }
+ DefaultPhoneAccountHandle defaultPhoneAccountHandle = mState.defaultOutgoingAccountHandles
+ .get(userHandle);
+ if (defaultPhoneAccountHandle == null) {
+ return null;
+ }
+ // Make sure the account is still registered and owned by the user.
+ PhoneAccount account = getPhoneAccount(defaultPhoneAccountHandle.phoneAccountHandle,
+ userHandle);
+
if (account != null) {
- return mState.defaultOutgoing;
+ return defaultPhoneAccountHandle.phoneAccountHandle;
}
return null;
}
@@ -228,13 +247,16 @@
* Sets the phone account with which to place all calls by default. Set by the user
* within phone settings.
*/
- public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+ public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle,
+ UserHandle userHandle) {
+ if (userHandle == null) {
+ return;
+ }
if (accountHandle == null) {
// Asking to clear the default outgoing is a valid request
- mState.defaultOutgoing = null;
+ mState.defaultOutgoingAccountHandles.remove(userHandle);
} else {
- // TODO: Do we really want to return for *any* user?
- PhoneAccount account = getPhoneAccount(accountHandle);
+ PhoneAccount account = getPhoneAccount(accountHandle, userHandle);
if (account == null) {
Log.w(this, "Trying to set nonexistent default outgoing %s",
accountHandle);
@@ -254,7 +276,8 @@
mSubscriptionManager.setDefaultVoiceSubId(subId);
}
- mState.defaultOutgoing = accountHandle;
+ mState.defaultOutgoingAccountHandles
+ .put(userHandle, new DefaultPhoneAccountHandle(userHandle, accountHandle));
}
write();
@@ -266,26 +289,6 @@
SubscriptionManager.getDefaultSmsSubscriptionId();
}
- /**
- * Returns the {@link PhoneAccountHandle} corresponding to the currently active SIM Call
- * Manager. SIM Call Manager returned corresponds to the following priority order:
- * 1. If a SIM Call Manager {@link PhoneAccount} is registered for the same package as the
- * default dialer, then that one is returned.
- * 2. If there is a SIM Call Manager {@link PhoneAccount} registered which matches the
- * carrier configuration's default, then that one is returned.
- * 3. Otherwise, we return null.
- */
- public PhoneAccountHandle getSimCallManager() {
- long token = Binder.clearCallingIdentity();
- int user;
- try {
- user = ActivityManager.getCurrentUser();
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- return getSimCallManager(user);
- }
-
public ComponentName getSystemSimCallManagerComponent() {
String defaultSimCallManager = null;
CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
@@ -299,6 +302,10 @@
? null : ComponentName.unflattenFromString(defaultSimCallManager);
}
+ public PhoneAccountHandle getSimCallManagerOfCurrentUser() {
+ return getSimCallManager(mCurrentUserHandle);
+ }
+
/**
* Returns the {@link PhoneAccountHandle} corresponding to the currently active SIM Call
* Manager. SIM Call Manager returned corresponds to the following priority order:
@@ -308,9 +315,10 @@
* carrier configuration's default, then that one is returned.
* 3. Otherwise, we return null.
*/
- public PhoneAccountHandle getSimCallManager(int user) {
+ public PhoneAccountHandle getSimCallManager(UserHandle userHandle) {
// Get the default dialer in case it has a connection manager associated with it.
- String dialerPackage = DefaultDialerManager.getDefaultDialerApplication(mContext, user);
+ String dialerPackage = DefaultDialerManager
+ .getDefaultDialerApplication(mContext, userHandle.getIdentifier());
// Check carrier config.
ComponentName systemSimCallManagerComponent = getSystemSimCallManagerComponent();
@@ -322,7 +330,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 */);
+ true /* includeDisabledAccounts */, userHandle);
for (PhoneAccountHandle accountHandle : allSimCallManagers) {
ComponentName component = accountHandle.getComponentName();
@@ -350,6 +358,23 @@
}
/**
+ * If it is a outgoing call, sim call manager of call-initiating user is returned.
+ * Otherwise, we return the sim call manager of the user associated with the
+ * target phone account.
+ * @return phone account handle of sim call manager based on the ongoing call.
+ */
+ public PhoneAccountHandle getSimCallManagerFromCall(Call call) {
+ if (call == null) {
+ return null;
+ }
+ UserHandle userHandle = call.getInitiatingUser();
+ if (userHandle == null) {
+ userHandle = call.getTargetPhoneAccount().getUserHandle();
+ }
+ return getSimCallManager(userHandle);
+ }
+
+ /**
* Update the current UserHandle to track when users are switched. This will allow the
* PhoneAccountRegistar to self-filter the PhoneAccounts to make sure we don't leak anything
* across users.
@@ -373,7 +398,7 @@
* otherwise.
*/
public boolean enablePhoneAccount(PhoneAccountHandle accountHandle, boolean isEnabled) {
- PhoneAccount account = getPhoneAccount(accountHandle);
+ PhoneAccount account = getPhoneAccountUnchecked(accountHandle);
if (account == null) {
Log.w(this, "Could not find account to enable: " + accountHandle);
return false;
@@ -385,17 +410,38 @@
if (account.isEnabled() != isEnabled) {
account.setIsEnabled(isEnabled);
+ if (!isEnabled) {
+ // If the disabled account is the default, remove it.
+ removeDefaultPhoneAccountHandle(accountHandle);
+ }
write();
fireAccountsChanged();
}
return true;
}
- private boolean isVisibleForUser(PhoneAccount account) {
+ private void removeDefaultPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) {
+ Iterator<Map.Entry<UserHandle, DefaultPhoneAccountHandle>> iterator =
+ mState.defaultOutgoingAccountHandles.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<UserHandle, DefaultPhoneAccountHandle> entry = iterator.next();
+ if (phoneAccountHandle.equals(entry.getValue().phoneAccountHandle)) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private boolean isVisibleForUser(PhoneAccount account, UserHandle userHandle,
+ boolean acrossProfiles) {
if (account == null) {
return false;
}
+ if (userHandle == null) {
+ Log.w(this, "userHandle is null in isVisibleForUser");
+ return false;
+ }
+
// If this PhoneAccount has CAPABILITY_MULTI_USER, it should be visible to all users and
// all profiles. Only Telephony and SIP accounts should have this capability.
if (account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
@@ -408,34 +454,22 @@
}
if (mCurrentUserHandle == null) {
+ // In case we need to have emergency phone calls from the lock screen.
Log.d(this, "Current user is null; assuming true");
return true;
}
- if (phoneAccountUserHandle.equals(Binder.getCallingUserHandle())) {
- return true;
+ if (acrossProfiles) {
+ return UserManager.get(mContext).isSameProfileGroup(userHandle.getIdentifier(),
+ phoneAccountUserHandle.getIdentifier());
+ } else {
+ return phoneAccountUserHandle.equals(userHandle);
}
-
- // Special check for work profiles.
- // Unlike in TelecomServiceImpl, we only care about *profiles* here. We want to make sure
- // that we don't resolve PhoneAccount across *users*, but resolving across *profiles* is
- // fine.
- if (UserHandle.getCallingUserId() == UserHandle.USER_OWNER) {
- List<UserInfo> profileUsers =
- mUserManager.getProfiles(mCurrentUserHandle.getIdentifier());
- for (UserInfo profileInfo : profileUsers) {
- if (profileInfo.getUserHandle().equals(phoneAccountUserHandle)) {
- return true;
- }
- }
- }
-
- return false;
}
private List<ResolveInfo> resolveComponent(PhoneAccountHandle phoneAccountHandle) {
return resolveComponent(phoneAccountHandle.getComponentName(),
- phoneAccountHandle.getUserHandle());
+ phoneAccountHandle.getUserHandle());
}
private List<ResolveInfo> resolveComponent(ComponentName componentName,
@@ -461,12 +495,16 @@
*
* @return The list of {@link PhoneAccountHandle}s.
*/
- public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
- return getPhoneAccountHandles(0, null, null, false);
+ public List<PhoneAccountHandle> getAllPhoneAccountHandles(UserHandle userHandle) {
+ return getPhoneAccountHandles(0, null, null, false, userHandle);
}
- public List<PhoneAccount> getAllPhoneAccounts() {
- return getPhoneAccounts(0, null, null, false);
+ public List<PhoneAccount> getAllPhoneAccounts(UserHandle userHandle) {
+ return getPhoneAccounts(0, null, null, false, userHandle);
+ }
+
+ public List<PhoneAccount> getAllPhoneAccountsOfCurrentUser() {
+ return getAllPhoneAccounts(mCurrentUserHandle);
}
/**
@@ -477,30 +515,40 @@
* @return The phone account handles.
*/
public List<PhoneAccountHandle> getCallCapablePhoneAccounts(
- String uriScheme, boolean includeDisabledAccounts) {
+ String uriScheme, boolean includeDisabledAccounts, UserHandle userHandle) {
return getPhoneAccountHandles(
PhoneAccount.CAPABILITY_CALL_PROVIDER,
PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY /*excludedCapabilities*/,
- uriScheme, null, includeDisabledAccounts);
+ uriScheme, null, includeDisabledAccounts, userHandle);
+ }
+
+ public List<PhoneAccountHandle> getCallCapablePhoneAccountsOfCurrentUser(
+ String uriScheme, boolean includeDisabledAccounts) {
+ return getCallCapablePhoneAccounts(uriScheme, includeDisabledAccounts, mCurrentUserHandle);
}
/**
* Retrieves a list of all the SIM-based phone accounts.
*/
- public List<PhoneAccountHandle> getSimPhoneAccounts() {
+ public List<PhoneAccountHandle> getSimPhoneAccounts(UserHandle userHandle) {
return getPhoneAccountHandles(
PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION,
- null, null, false);
+ null, null, false, userHandle);
}
- /**
- * Retrieves a list of all phone accounts registered by a specified package.
- *
- * @param packageName The name of the package that registered the phone accounts.
- * @return The phone account handles.
- */
- public List<PhoneAccountHandle> getPhoneAccountsForPackage(String packageName) {
- return getPhoneAccountHandles(0, null, packageName, false);
+ public List<PhoneAccountHandle> getSimPhoneAccountsOfCurrentUser() {
+ return getSimPhoneAccounts(mCurrentUserHandle);
+ }
+
+ /**
+ * Retrieves a list of all phone accounts registered by a specified package.
+ *
+ * @param packageName The name of the package that registered the phone accounts.
+ * @return The phone account handles.
+ */
+ public List<PhoneAccountHandle> getPhoneAccountsForPackage(String packageName,
+ UserHandle userHandle) {
+ return getPhoneAccountHandles(0, null, packageName, false, userHandle);
}
// TODO: Should we implement an artificial limit for # of accounts associated with a single
@@ -533,7 +581,7 @@
// source app provides or else an third party app could enable itself.
boolean isEnabled = false;
- PhoneAccount oldAccount = getPhoneAccount(account.getAccountHandle());
+ PhoneAccount oldAccount = getPhoneAccountUnchecked(account.getAccountHandle());
if (oldAccount != null) {
mState.accounts.remove(oldAccount);
isEnabled = oldAccount.isEnabled();
@@ -553,7 +601,7 @@
}
public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
- PhoneAccount account = getPhoneAccount(accountHandle);
+ PhoneAccount account = getPhoneAccountUnchecked(accountHandle);
if (account != null) {
if (mState.accounts.remove(account)) {
write();
@@ -615,12 +663,6 @@
}
}
- private void fireSimCallManagerChanged() {
- for (Listener l : mListeners) {
- l.onSimCallManagerChanged(this);
- }
- }
-
private String getAccountDiffString(PhoneAccount account1, PhoneAccount account2) {
if (account1 == null || account2 == null) {
return "Diff: " + account1 + ", " + account2;
@@ -696,7 +738,7 @@
* @param handle
* @return The corresponding phone account if one exists.
*/
- PhoneAccount getPhoneAccount(PhoneAccountHandle handle) {
+ public PhoneAccount getPhoneAccountUnchecked(PhoneAccountHandle handle) {
for (PhoneAccount m : mState.accounts) {
if (Objects.equals(handle, m.getAccountHandle())) {
return m;
@@ -710,21 +752,31 @@
* account before returning it. The current user is the active user on the actual android
* device.
*/
- public PhoneAccount getPhoneAccountCheckCallingUser(PhoneAccountHandle handle) {
- PhoneAccount account = getPhoneAccount(handle);
- if (account != null && isVisibleForUser(account)) {
+ public PhoneAccount getPhoneAccount(PhoneAccountHandle handle, UserHandle userHandle) {
+ return getPhoneAccount(handle, userHandle, /* acrossProfiles */ false);
+ }
+
+ public PhoneAccount getPhoneAccount(PhoneAccountHandle handle,
+ UserHandle userHandle, boolean acrossProfiles) {
+ PhoneAccount account = getPhoneAccountUnchecked(handle);
+ if (account != null && (isVisibleForUser(account, userHandle, acrossProfiles))) {
return account;
}
return null;
}
+ public PhoneAccount getPhoneAccountOfCurrentUser(PhoneAccountHandle handle) {
+ return getPhoneAccount(handle, mCurrentUserHandle);
+ }
+
private List<PhoneAccountHandle> getPhoneAccountHandles(
int capabilities,
String uriScheme,
String packageName,
- boolean includeDisabledAccounts) {
+ boolean includeDisabledAccounts,
+ UserHandle userHandle) {
return getPhoneAccountHandles(capabilities, 0 /*excludedCapabilities*/, uriScheme,
- packageName, includeDisabledAccounts);
+ packageName, includeDisabledAccounts, userHandle);
}
/**
@@ -736,12 +788,13 @@
int excludedCapabilities,
String uriScheme,
String packageName,
- boolean includeDisabledAccounts) {
+ boolean includeDisabledAccounts,
+ UserHandle userHandle) {
List<PhoneAccountHandle> handles = new ArrayList<>();
for (PhoneAccount account : getPhoneAccounts(
capabilities, excludedCapabilities, uriScheme, packageName,
- includeDisabledAccounts)) {
+ includeDisabledAccounts, userHandle)) {
handles.add(account.getAccountHandle());
}
return handles;
@@ -751,9 +804,10 @@
int capabilities,
String uriScheme,
String packageName,
- boolean includeDisabledAccounts) {
+ boolean includeDisabledAccounts,
+ UserHandle userHandle) {
return getPhoneAccounts(capabilities, 0 /*excludedCapabilities*/, uriScheme, packageName,
- includeDisabledAccounts);
+ includeDisabledAccounts, userHandle);
}
/**
@@ -772,7 +826,8 @@
int excludedCapabilities,
String uriScheme,
String packageName,
- boolean includeDisabledAccounts) {
+ boolean includeDisabledAccounts,
+ UserHandle userHandle) {
List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
for (PhoneAccount m : mState.accounts) {
if (!(m.isEnabled() || includeDisabledAccounts)) {
@@ -804,7 +859,7 @@
// Not the right package name; skip this one.
continue;
}
- if (!isVisibleForUser(m)) {
+ if (!isVisibleForUser(m, userHandle, false)) {
// Account is not visible for the current user; skip this one.
continue;
}
@@ -823,10 +878,11 @@
@VisibleForTesting
public static class State {
/**
- * The account selected by the user to be employed by default for making outgoing calls.
- * If the user has not made such a selection, then this is null.
+ * Store the default phone account handle of users. If no record of a user can be found in
+ * the map, it means that no default phone account handle is set in that user.
*/
- public PhoneAccountHandle defaultOutgoing = null;
+ public final Map<UserHandle, DefaultPhoneAccountHandle> defaultOutgoingAccountHandles
+ = new ConcurrentHashMap<>();
/**
* The complete list of {@code PhoneAccount}s known to the Telecom subsystem.
@@ -840,6 +896,22 @@
}
/**
+ * The default {@link PhoneAccountHandle} of a user.
+ */
+ public static class DefaultPhoneAccountHandle {
+
+ public final UserHandle userHandle;
+
+ public final PhoneAccountHandle phoneAccountHandle;
+
+ public DefaultPhoneAccountHandle(UserHandle userHandle,
+ PhoneAccountHandle phoneAccountHandle) {
+ this.userHandle = userHandle;
+ this.phoneAccountHandle = phoneAccountHandle;
+ }
+ }
+
+ /**
* Dumps the state of the {@link CallsManager}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
@@ -847,9 +919,11 @@
public void dump(IndentingPrintWriter pw) {
if (mState != null) {
pw.println("xmlVersion: " + mState.versionNumber);
- pw.println("defaultOutgoing: " + (mState.defaultOutgoing == null ? "none" :
- mState.defaultOutgoing));
- pw.println("simCallManager: " + getSimCallManager());
+ DefaultPhoneAccountHandle defaultPhoneAccountHandle
+ = mState.defaultOutgoingAccountHandles.get(Process.myUserHandle());
+ pw.println("defaultOutgoing: " + (defaultPhoneAccountHandle == null ? "none" :
+ defaultPhoneAccountHandle.phoneAccountHandle));
+ pw.println("simCallManager: " + getSimCallManager(mCurrentUserHandle));
pw.println("phoneAccounts:");
pw.increaseIndent();
for (PhoneAccount phoneAccount : mState.accounts) {
@@ -864,26 +938,35 @@
// State management
//
- private void write() {
- final FileOutputStream os;
- try {
- os = mAtomicFile.startWrite();
- boolean success = false;
+ private class AsyncXmlWriter extends AsyncTask<ByteArrayOutputStream, Void, Void> {
+ @Override
+ public Void doInBackground(ByteArrayOutputStream... args) {
+ final ByteArrayOutputStream buffer = args[0];
+ FileOutputStream fileOutput = null;
try {
- XmlSerializer serializer = new FastXmlSerializer();
- serializer.setOutput(new BufferedOutputStream(os), "utf-8");
- writeToXml(mState, serializer, mContext);
- serializer.flush();
- success = true;
- } finally {
- if (success) {
- mAtomicFile.finishWrite(os);
- } else {
- mAtomicFile.failWrite(os);
+ synchronized (mWriteLock) {
+ fileOutput = mAtomicFile.startWrite();
+ buffer.writeTo(fileOutput);
+ mAtomicFile.finishWrite(fileOutput);
}
+ } catch (IOException e) {
+ Log.e(this, e, "Writing state to XML file");
+ mAtomicFile.failWrite(fileOutput);
}
+ return null;
+ }
+ }
+
+ private void write() {
+ try {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ XmlSerializer serializer = new FastXmlSerializer();
+ serializer.setOutput(os, "utf-8");
+ writeToXml(mState, serializer, mContext);
+ serializer.flush();
+ new AsyncXmlWriter().execute(os);
} catch (IOException e) {
- Log.e(this, e, "Writing state to XML file");
+ Log.e(this, e, "Writing state to XML buffer");
}
}
@@ -954,8 +1037,13 @@
@VisibleForTesting
public abstract static class XmlSerialization<T> {
- private static final String LENGTH_ATTRIBUTE = "length";
- private static final String VALUE_TAG = "value";
+ private static final String TAG_VALUE = "value";
+ private static final String ATTRIBUTE_LENGTH = "length";
+ private static final String ATTRIBUTE_KEY = "key";
+ private static final String ATTRIBUTE_VALUE_TYPE = "type";
+ private static final String VALUE_TYPE_STRING = "string";
+ private static final String VALUE_TYPE_INTEGER = "integer";
+ private static final String VALUE_TYPE_BOOLEAN = "boolean";
/**
* Write the supplied object to XML
@@ -996,16 +1084,51 @@
serializer.startTag(null, tagName);
if (values != null) {
- serializer.attribute(null, LENGTH_ATTRIBUTE, Objects.toString(values.size()));
+ serializer.attribute(null, ATTRIBUTE_LENGTH, Objects.toString(values.size()));
for (String toSerialize : values) {
- serializer.startTag(null, VALUE_TAG);
+ serializer.startTag(null, TAG_VALUE);
if (toSerialize != null ){
serializer.text(toSerialize);
}
- serializer.endTag(null, VALUE_TAG);
+ serializer.endTag(null, TAG_VALUE);
}
} else {
- serializer.attribute(null, LENGTH_ATTRIBUTE, "0");
+ serializer.attribute(null, ATTRIBUTE_LENGTH, "0");
+ }
+ serializer.endTag(null, tagName);
+ }
+
+ protected void writeBundle(String tagName, Bundle values, XmlSerializer serializer)
+ throws IOException {
+
+ serializer.startTag(null, tagName);
+ if (values != null) {
+ for (String key : values.keySet()) {
+ Object value = values.get(key);
+
+ if (value == null) {
+ continue;
+ }
+
+ String valueType;
+ if (value instanceof String) {
+ valueType = VALUE_TYPE_STRING;
+ } else if (value instanceof Integer) {
+ valueType = VALUE_TYPE_INTEGER;
+ } else if (value instanceof Boolean) {
+ valueType = VALUE_TYPE_BOOLEAN;
+ } else {
+ Log.w(this,
+ "PhoneAccounts support only string, integer and boolean extras TY.");
+ continue;
+ }
+
+ serializer.startTag(null, TAG_VALUE);
+ serializer.attribute(null, ATTRIBUTE_KEY, key);
+ serializer.attribute(null, ATTRIBUTE_VALUE_TYPE, valueType);
+ serializer.text(Objects.toString(value));
+ serializer.endTag(null, TAG_VALUE);
+ }
}
serializer.endTag(null, tagName);
}
@@ -1042,7 +1165,7 @@
protected List<String> readStringList(XmlPullParser parser)
throws IOException, XmlPullParserException {
- int length = Integer.parseInt(parser.getAttributeValue(null, LENGTH_ATTRIBUTE));
+ int length = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_LENGTH));
List<String> arrayEntries = new ArrayList<String>(length);
String value = null;
@@ -1052,7 +1175,7 @@
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (parser.getName().equals(VALUE_TAG)) {
+ if (parser.getName().equals(TAG_VALUE)) {
parser.next();
value = parser.getText();
arrayEntries.add(value);
@@ -1062,6 +1185,55 @@
return arrayEntries;
}
+ /**
+ * Reads a bundle from the XML parser.
+ *
+ * @param parser The XML parser.
+ * @return Bundle containing the parsed values.
+ * @throws IOException Exception related to IO.
+ * @throws XmlPullParserException Exception related to parsing.
+ */
+ protected Bundle readBundle(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+
+ Bundle bundle = null;
+ int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(TAG_VALUE)) {
+ String valueType = parser.getAttributeValue(null, ATTRIBUTE_VALUE_TYPE);
+ String key = parser.getAttributeValue(null, ATTRIBUTE_KEY);
+ parser.next();
+ String value = parser.getText();
+
+ if (bundle == null) {
+ bundle = new Bundle();
+ }
+
+ // Do not write null values to the bundle.
+ if (value == null) {
+ continue;
+ }
+
+ if (VALUE_TYPE_STRING.equals(valueType)) {
+ bundle.putString(key, value);
+ } else if (VALUE_TYPE_INTEGER.equals(valueType)) {
+ try {
+ int intValue = Integer.parseInt(value);
+ bundle.putInt(key, intValue);
+ } catch (NumberFormatException nfe) {
+ Log.w(this, "Invalid integer PhoneAccount extra.");
+ }
+ } else if (VALUE_TYPE_BOOLEAN.equals(valueType)) {
+ boolean boolValue = Boolean.parseBoolean(value);
+ bundle.putBoolean(key, boolValue);
+ } else {
+ Log.w(this, "Invalid type " + valueType + " for PhoneAccount bundle.");
+ }
+ }
+ }
+ return bundle;
+ }
+
protected Bitmap readBitmap(XmlPullParser parser) {
byte[] imageByteArray = Base64.decode(parser.getText(), 0);
return BitmapFactory.decodeByteArray(imageByteArray, 0, imageByteArray.length);
@@ -1089,11 +1261,13 @@
serializer.startTag(null, CLASS_STATE);
serializer.attribute(null, VERSION, Objects.toString(EXPECTED_STATE_VERSION));
- if (o.defaultOutgoing != null) {
- serializer.startTag(null, DEFAULT_OUTGOING);
- sPhoneAccountHandleXml.writeToXml(o.defaultOutgoing, serializer, context);
- serializer.endTag(null, DEFAULT_OUTGOING);
+ serializer.startTag(null, DEFAULT_OUTGOING);
+ for (DefaultPhoneAccountHandle defaultPhoneAccountHandle : o
+ .defaultOutgoingAccountHandles.values()) {
+ sDefaultPhoneAcountHandleXml
+ .writeToXml(defaultPhoneAccountHandle, serializer, context);
}
+ serializer.endTag(null, DEFAULT_OUTGOING);
serializer.startTag(null, ACCOUNTS);
for (PhoneAccount m : o.accounts) {
@@ -1112,15 +1286,39 @@
State s = new State();
String rawVersion = parser.getAttributeValue(null, VERSION);
- s.versionNumber = TextUtils.isEmpty(rawVersion) ? 1 :
- Integer.parseInt(rawVersion);
+ s.versionNumber = TextUtils.isEmpty(rawVersion) ? 1 : Integer.parseInt(rawVersion);
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (parser.getName().equals(DEFAULT_OUTGOING)) {
- parser.nextTag();
- s.defaultOutgoing = sPhoneAccountHandleXml.readFromXml(parser,
- s.versionNumber, context);
+ if (s.versionNumber < 9) {
+ // Migration old default phone account handle here by assuming the
+ // default phone account handle is belong to primary user.
+ parser.nextTag();
+ PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
+ .readFromXml(parser, s.versionNumber, context);
+ UserManager userManager = UserManager.get(context);
+ UserInfo primaryUser = userManager.getPrimaryUser();
+ if (primaryUser != null) {
+ UserHandle userHandle = primaryUser.getUserHandle();
+ DefaultPhoneAccountHandle defaultPhoneAccountHandle
+ = new DefaultPhoneAccountHandle(userHandle,
+ phoneAccountHandle);
+ s.defaultOutgoingAccountHandles
+ .put(userHandle, defaultPhoneAccountHandle);
+ }
+ } else {
+ int defaultAccountHandlesDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, defaultAccountHandlesDepth)) {
+ DefaultPhoneAccountHandle accountHandle
+ = sDefaultPhoneAcountHandleXml
+ .readFromXml(parser, s.versionNumber, context);
+ if (accountHandle != null && s.accounts != null) {
+ s.defaultOutgoingAccountHandles
+ .put(accountHandle.userHandle, accountHandle);
+ }
+ }
+ }
} else if (parser.getName().equals(ACCOUNTS)) {
int accountsDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
@@ -1140,6 +1338,70 @@
};
@VisibleForTesting
+ public static final XmlSerialization<DefaultPhoneAccountHandle> sDefaultPhoneAcountHandleXml =
+ new XmlSerialization<DefaultPhoneAccountHandle>() {
+ private static final String CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE
+ = "default_outgoing_phone_account_handle";
+ private static final String USER_SERIAL_NUMBER = "user_serial_number";
+ private static final String ACCOUNT_HANDLE = "account_handle";
+
+ @Override
+ public void writeToXml(DefaultPhoneAccountHandle o, XmlSerializer serializer,
+ Context context) throws IOException {
+ if (o != null) {
+ final UserManager userManager = UserManager.get(context);
+ final long serialNumber = userManager.getSerialNumberForUser(o.userHandle);
+ if (serialNumber != -1) {
+ serializer.startTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
+ writeLong(USER_SERIAL_NUMBER, serialNumber, serializer);
+ serializer.startTag(null, ACCOUNT_HANDLE);
+ sPhoneAccountHandleXml.writeToXml(o.phoneAccountHandle, serializer,
+ context);
+ serializer.endTag(null, ACCOUNT_HANDLE);
+ serializer.endTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
+ }
+ }
+ }
+
+ @Override
+ public DefaultPhoneAccountHandle readFromXml(XmlPullParser parser, int version,
+ Context context)
+ throws IOException, XmlPullParserException {
+ if (parser.getName().equals(CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE)) {
+ int outerDepth = parser.getDepth();
+ PhoneAccountHandle accountHandle = null;
+ String userSerialNumberString = null;
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals(ACCOUNT_HANDLE)) {
+ parser.nextTag();
+ accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
+ context);
+ } else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
+ parser.next();
+ userSerialNumberString = parser.getText();
+ }
+ }
+ UserHandle userHandle = null;
+ if (userSerialNumberString != null) {
+ try {
+ long serialNumber = Long.parseLong(userSerialNumberString);
+ userHandle = UserManager.get(context)
+ .getUserForSerialNumber(serialNumber);
+ } catch (NumberFormatException e) {
+ Log.e(this, e,
+ "Could not parse UserHandle " + userSerialNumberString);
+ }
+ }
+ if (accountHandle != null && userHandle != null) {
+ return new DefaultPhoneAccountHandle(userHandle, accountHandle);
+ }
+ }
+ return null;
+ }
+ };
+
+
+ @VisibleForTesting
public static final XmlSerialization<PhoneAccount> sPhoneAccountXml =
new XmlSerialization<PhoneAccount>() {
private static final String CLASS_PHONE_ACCOUNT = "phone_account";
@@ -1156,6 +1418,7 @@
private static final String SHORT_DESCRIPTION = "short_description";
private static final String SUPPORTED_URI_SCHEMES = "supported_uri_schemes";
private static final String ICON = "icon";
+ private static final String EXTRAS = "extras";
private static final String ENABLED = "enabled";
@Override
@@ -1179,6 +1442,7 @@
writeTextIfNonNull(LABEL, o.getLabel(), serializer);
writeTextIfNonNull(SHORT_DESCRIPTION, o.getShortDescription(), serializer);
writeStringList(SUPPORTED_URI_SCHEMES, o.getSupportedUriSchemes(), serializer);
+ writeBundle(EXTRAS, o.getExtras(), serializer);
writeTextIfNonNull(ENABLED, o.isEnabled() ? "true" : "false" , serializer);
serializer.endTag(null, CLASS_PHONE_ACCOUNT);
@@ -1203,6 +1467,7 @@
List<String> supportedUriSchemes = null;
Icon icon = null;
boolean enabled = false;
+ Bundle extras = null;
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (parser.getName().equals(ACCOUNT_HANDLE)) {
@@ -1248,6 +1513,8 @@
} else if (parser.getName().equals(ENABLED)) {
parser.next();
enabled = "true".equalsIgnoreCase(parser.getText());
+ } else if (parser.getName().equals(EXTRAS)) {
+ extras = readBundle(parser);
}
}
@@ -1313,6 +1580,7 @@
.setShortDescription(shortDescription)
.setSupportedUriSchemes(supportedUriSchemes)
.setHighlightColor(highlightColor)
+ .setExtras(extras)
.setIsEnabled(enabled);
if (icon != null) {
diff --git a/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java b/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
new file mode 100644
index 0000000..41284cb
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Intent;
+
+/**
+ * Interface to avoid static calls to PhoneNumberUtils. Add methods to this interface as needed for
+ * refactoring.
+ */
+public interface PhoneNumberUtilsAdapter {
+ boolean isPotentialLocalEmergencyNumber(Context context, String number);
+ boolean isUriNumber(String number);
+ String getNumberFromIntent(Intent intent, Context context);
+ String convertKeypadLettersToDigits(String number);
+ String stripSeparators(String number);
+}
diff --git a/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java b/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
new file mode 100644
index 0000000..640d814
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Intent;
+import android.telephony.PhoneNumberUtils;
+
+public class PhoneNumberUtilsAdapterImpl implements PhoneNumberUtilsAdapter {
+ @Override
+ public boolean isPotentialLocalEmergencyNumber(Context context, String number) {
+ return PhoneNumberUtils.isPotentialLocalEmergencyNumber(context, number);
+ }
+
+ @Override
+ public boolean isUriNumber(String number) {
+ return PhoneNumberUtils.isUriNumber(number);
+ }
+
+ @Override
+ public String getNumberFromIntent(Intent intent, Context context) {
+ return PhoneNumberUtils.getNumberFromIntent(intent, context);
+ }
+
+ @Override
+ public String convertKeypadLettersToDigits(String number) {
+ return PhoneNumberUtils.convertKeypadLettersToDigits(number);
+ }
+
+ @Override
+ public String stripSeparators(String number) {
+ return PhoneNumberUtils.stripSeparators(number);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 57ae24b..54329c8 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -43,26 +43,20 @@
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
- if ((newState == CallState.DIALING || newState == CallState.ACTIVE
- || newState == CallState.ON_HOLD) &&
- !mCallsManager.hasRingingCall()) {
- /*
- * EXTRA_STATE_RINGING takes precedence over EXTRA_STATE_OFFHOOK, so if there is
- * already a ringing call, don't broadcast EXTRA_STATE_OFFHOOK.
- */
- sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_OFFHOOK);
- }
+ updateStates(call);
}
@Override
public void onCallAdded(Call call) {
- if (call.getState() == CallState.RINGING) {
- sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_RINGING);
- }
- };
+ updateStates(call);
+ }
@Override
public void onCallRemoved(Call call) {
+ updateStates(call);
+ }
+
+ private void updateStates(Call call) {
// Recalculate the current phone state based on the consolidated state of the remaining
// calls in the call list.
int callState = TelephonyManager.CALL_STATE_IDLE;
diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java
index 5fddb89..dd336c4 100644
--- a/src/com/android/server/telecom/ProximitySensorManager.java
+++ b/src/com/android/server/telecom/ProximitySensorManager.java
@@ -19,27 +19,21 @@
import android.content.Context;
import android.os.PowerManager;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* This class manages the proximity sensor and allows callers to turn it on and off.
*/
public class ProximitySensorManager extends CallsManagerListenerBase {
- private static final String TAG = ProximitySensorManager.class.getSimpleName();
- private final PowerManager.WakeLock mProximityWakeLock;
private final CallsManager mCallsManager;
+ private final TelecomWakeLock mTelecomWakeLock;
- public ProximitySensorManager(Context context, CallsManager callsManager) {
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ public ProximitySensorManager(TelecomWakeLock telecomWakeLock, CallsManager callsManager) {
- if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
- mProximityWakeLock = pm.newWakeLock(
- PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
- } else {
- mProximityWakeLock = null;
- }
-
+ mTelecomWakeLock = telecomWakeLock;
mCallsManager = callsManager;
- Log.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock);
+ Log.d(this, "onCreate: mProximityWakeLock: ", mTelecomWakeLock);
}
@Override
@@ -54,38 +48,23 @@
/**
* Turn the proximity sensor on.
*/
- void turnOn() {
+ @VisibleForTesting
+ public void turnOn() {
if (mCallsManager.getCalls().isEmpty()) {
Log.w(this, "Asking to turn on prox sensor without a call? I don't think so.");
return;
}
- if (mProximityWakeLock == null) {
- return;
- }
- if (!mProximityWakeLock.isHeld()) {
- Log.i(this, "Acquiring proximity wake lock");
- mProximityWakeLock.acquire();
- } else {
- Log.i(this, "Proximity wake lock already acquired");
- }
+ mTelecomWakeLock.acquire();
}
/**
* Turn the proximity sensor off.
* @param screenOnImmediately
*/
- void turnOff(boolean screenOnImmediately) {
- if (mProximityWakeLock == null) {
- return;
- }
- if (mProximityWakeLock.isHeld()) {
- Log.i(this, "Releasing proximity wake lock");
- int flags =
- (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
- mProximityWakeLock.release(flags);
- } else {
- Log.i(this, "Proximity wake lock already released");
- }
+ @VisibleForTesting
+ public void turnOff(boolean screenOnImmediately) {
+ int flags = (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
+ mTelecomWakeLock.release(flags);
}
}
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
index bb2055f..af60b68 100644
--- a/src/com/android/server/telecom/RingbackPlayer.java
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -1,17 +1,17 @@
/*
- * Copyright 2014, The Android Open Source Project
+ * Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
package com.android.server.telecom;
@@ -24,9 +24,7 @@
* able to turn off and on as the user switches between calls. This is why it is implemented as its
* own class.
*/
-class RingbackPlayer extends CallsManagerListenerBase {
-
- private final CallsManager mCallsManager;
+class RingbackPlayer {
private final InCallTonePlayer.Factory mPlayerFactory;
@@ -40,52 +38,16 @@
*/
private InCallTonePlayer mTonePlayer;
- RingbackPlayer(CallsManager callsManager, InCallTonePlayer.Factory playerFactory) {
- mCallsManager = callsManager;
+ RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
mPlayerFactory = playerFactory;
}
- /** {@inheritDoc} */
- @Override
- public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
- if (oldForegroundCall != null) {
- stopRingbackForCall(oldForegroundCall);
- }
-
- if (shouldStartRinging(newForegroundCall)) {
- startRingbackForCall(newForegroundCall);
- }
- }
-
- @Override
- public void onConnectionServiceChanged(
- Call call,
- ConnectionServiceWrapper oldService,
- ConnectionServiceWrapper newService) {
-
- // Treat as ending or begining dialing based on the state transition.
- if (shouldStartRinging(call)) {
- startRingbackForCall(call);
- } else if (newService == null) {
- stopRingbackForCall(call);
- }
- }
-
- @Override
- public void onRingbackRequested(Call call, boolean ignored) {
- if (shouldStartRinging(call)) {
- startRingbackForCall(call);
- } else {
- stopRingbackForCall(call);
- }
- }
-
/**
* Starts ringback for the specified dialing call as needed.
*
* @param call The call for which to ringback.
*/
- private void startRingbackForCall(Call call) {
+ public void startRingbackForCall(Call call) {
Preconditions.checkState(call.getState() == CallState.DIALING);
if (mCall == call) {
@@ -112,7 +74,7 @@
*
* @param call The call for which to stop ringback.
*/
- private void stopRingbackForCall(Call call) {
+ public void stopRingbackForCall(Call call) {
if (mCall == call) {
// The foreground call is no longer dialing or is no longer the foreground call. In
// either case, stop the ringback tone.
@@ -127,11 +89,4 @@
}
}
}
-
- private boolean shouldStartRinging(Call call) {
- return call != null
- && mCallsManager.getForegroundCall() == call
- && call.getState() == CallState.DIALING
- && call.isRingbackRequested();
- }
-}
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 0245859..a3766ec 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
package com.android.server.telecom;
@@ -23,28 +23,21 @@
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
-import android.os.SystemVibrator;
import android.os.Vibrator;
-import android.provider.Settings;
-import java.util.LinkedList;
-import java.util.List;
+import com.android.internal.annotations.VisibleForTesting;
/**
* Controls the ringtone player.
- * TODO: Turn this into a proper state machine: Ringing, CallWaiting, Stopped.
*/
-final class Ringer extends CallsManagerListenerBase {
+@VisibleForTesting
+public final class Ringer {
private static final long[] VIBRATION_PATTERN = new long[] {
0, // No delay before starting
1000, // How long to vibrate
1000, // How long to wait before vibrating again
};
- private static final int STATE_RINGING = 1;
- private static final int STATE_CALL_WAITING = 2;
- private static final int STATE_STOPPED = 3;
-
private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
@@ -53,22 +46,25 @@
/** Indicate that we want the pattern to repeat at the step which turns on vibration. */
private static final int VIBRATION_PATTERN_REPEAT = 1;
- private final AsyncRingtonePlayer mRingtonePlayer;
-
/**
* Used to keep ordering of unanswered incoming calls. There can easily exist multiple incoming
* calls and explicit ordering is useful for maintaining the proper state of the ringer.
*/
- private final List<Call> mRingingCalls = new LinkedList<>();
- private final CallAudioManager mCallAudioManager;
- private final CallsManager mCallsManager;
+ private final SystemSettingsUtil mSystemSettingsUtil;
private final InCallTonePlayer.Factory mPlayerFactory;
+ private final AsyncRingtonePlayer mRingtonePlayer;
private final Context mContext;
private final Vibrator mVibrator;
- private int mState = STATE_STOPPED;
private InCallTonePlayer mCallWaitingPlayer;
+ private RingtoneFactory mRingtoneFactory;
+
+ /**
+ * Call objects that are ringing or call-waiting. These are used only for logging purposes.
+ */
+ private Call mRingingCall;
+ private Call mCallWaitingCall;
/**
* Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls.
@@ -76,168 +72,115 @@
private boolean mIsVibrating = false;
/** Initializes the Ringer. */
- Ringer(
- CallAudioManager callAudioManager,
- CallsManager callsManager,
+ @VisibleForTesting
+ public Ringer(
InCallTonePlayer.Factory playerFactory,
- Context context) {
+ Context context,
+ SystemSettingsUtil systemSettingsUtil,
+ AsyncRingtonePlayer asyncRingtonePlayer,
+ RingtoneFactory ringtoneFactory,
+ Vibrator vibrator) {
- mCallAudioManager = callAudioManager;
- mCallsManager = callsManager;
+ mSystemSettingsUtil = systemSettingsUtil;
mPlayerFactory = playerFactory;
mContext = context;
// We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this
// vibrator object will be isolated from others.
- mVibrator = new SystemVibrator(context);
- mRingtonePlayer = new AsyncRingtonePlayer(context);
+ mVibrator = vibrator;
+ mRingtonePlayer = asyncRingtonePlayer;
+ mRingtoneFactory = ringtoneFactory;
}
- @Override
- public void onCallAdded(final Call call) {
- if (call.isIncoming() && call.getState() == CallState.RINGING) {
- if (mRingingCalls.contains(call)) {
- Log.wtf(this, "New ringing call is already in list of unanswered calls");
- }
- mRingingCalls.add(call);
- updateRinging(call);
- }
- }
-
- @Override
- public void onCallRemoved(Call call) {
- removeFromUnansweredCall(call);
- }
-
- @Override
- public void onCallStateChanged(Call call, int oldState, int newState) {
- if (newState != CallState.RINGING) {
- removeFromUnansweredCall(call);
- }
- }
-
- @Override
- public void onIncomingCallAnswered(Call call) {
- onRespondedToIncomingCall(call);
- }
-
- @Override
- public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
- onRespondedToIncomingCall(call);
- }
-
- @Override
- public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
- Call ringingCall = null;
- if (mRingingCalls.contains(newForegroundCall)) {
- ringingCall = newForegroundCall;
- } else if (mRingingCalls.contains(oldForegroundCall)) {
- ringingCall = oldForegroundCall;
- }
- if (ringingCall != null) {
- updateRinging(ringingCall);
- }
- }
-
- /**
- * Silences the ringer for any actively ringing calls.
- */
- void silence() {
- // Remove all calls from the "ringing" set and then update the ringer.
- mRingingCalls.clear();
- updateRinging(null);
- }
-
- private void onRespondedToIncomingCall(Call call) {
- // Only stop the ringer if this call is the top-most incoming call.
- if (getTopMostUnansweredCall() == call) {
- removeFromUnansweredCall(call);
- }
- }
-
- private Call getTopMostUnansweredCall() {
- return mRingingCalls.isEmpty() ? null : mRingingCalls.get(0);
- }
-
- /**
- * Removes the specified call from the list of unanswered incoming calls and updates the ringer
- * based on the new state of {@link #mRingingCalls}. Safe to call with a call that is not
- * present in the list of incoming calls.
- */
- private void removeFromUnansweredCall(Call call) {
- mRingingCalls.remove(call);
- updateRinging(call);
- }
-
- private void updateRinging(Call call) {
- if (mRingingCalls.isEmpty()) {
- stopRinging(call, "No more ringing calls found");
- stopCallWaiting(call);
- } else {
- startRingingOrCallWaiting(call);
- }
- }
-
- private void startRingingOrCallWaiting(Call call) {
- Call foregroundCall = mCallsManager.getForegroundCall();
- Log.v(this, "startRingingOrCallWaiting, foregroundCall: %s.", foregroundCall);
-
- if (Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.THEATER_MODE_ON,
- 0) == 1) {
+ public void startRinging(Call foregroundCall) {
+ if (mSystemSettingsUtil.isTheaterModeOn(mContext)) {
return;
}
- if (mRingingCalls.contains(foregroundCall)) {
- // The foreground call is one of incoming calls so play the ringer out loud.
- stopCallWaiting(call);
+ if (foregroundCall == null) {
+ Log.wtf(this, "startRinging called with null foreground call.");
+ return;
+ }
- if (!shouldRingForContact(foregroundCall.getContactUri())) {
- return;
+ if (InCallController.doesDefaultDialerSupportRinging(mContext)) {
+ Log.event(foregroundCall, Log.Events.SKIP_RINGING);
+ return;
+ }
+
+ stopCallWaiting();
+
+ if (!shouldRingForContact(foregroundCall.getContactUri())) {
+ return;
+ }
+
+ AudioManager audioManager =
+ (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+ mRingingCall = foregroundCall;
+ Log.event(foregroundCall, Log.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.
+ mRingtonePlayer.play(
+ mRingtoneFactory.getRingtone(foregroundCall.getRingtone()));
+ } else {
+ Log.v(this, "startRingingOrCallWaiting, skipping because volume is 0");
+ }
+
+ if (shouldVibrate(mContext) && !mIsVibrating) {
+ mVibrator.vibrate(VIBRATION_PATTERN, VIBRATION_PATTERN_REPEAT,
+ VIBRATION_ATTRIBUTES);
+ mIsVibrating = true;
+ }
+ }
+
+ public void startCallWaiting(Call call) {
+ if (mSystemSettingsUtil.isTheaterModeOn(mContext)) {
+ return;
+ }
+
+ if (InCallController.doesDefaultDialerSupportRinging(mContext)) {
+ Log.event(call, Log.Events.SKIP_RINGING);
+ return;
+ }
+
+ Log.v(this, "Playing call-waiting tone.");
+
+ stopRinging();
+
+ if (mCallWaitingPlayer == null) {
+ Log.event(call, Log.Events.START_CALL_WAITING_TONE);
+ mCallWaitingCall = call;
+ mCallWaitingPlayer =
+ mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+ mCallWaitingPlayer.startTone();
+ }
+ }
+
+ public void stopRinging() {
+ if (mRingingCall != null) {
+ Log.event(mRingingCall, Log.Events.STOP_RINGER);
+ mRingingCall = null;
+ }
+
+ mRingtonePlayer.stop();
+
+ if (mIsVibrating) {
+ mVibrator.cancel();
+ mIsVibrating = false;
+ }
+ }
+
+ public void stopCallWaiting() {
+ Log.v(this, "stop call waiting.");
+ if (mCallWaitingPlayer != null) {
+ if (mCallWaitingCall != null) {
+ Log.event(mCallWaitingCall, Log.Events.STOP_CALL_WAITING_TONE);
+ mCallWaitingCall = null;
}
- AudioManager audioManager =
- (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
- if (mState != STATE_RINGING) {
- Log.event(call, Log.Events.START_RINGER);
- mState = STATE_RINGING;
- }
- mCallAudioManager.setIsRinging(call, true);
-
- // 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.
- mRingtonePlayer.play(foregroundCall.getRingtone());
- } else {
- Log.v(this, "startRingingOrCallWaiting, skipping because volume is 0");
- }
-
- if (shouldVibrate(mContext) && !mIsVibrating) {
- mVibrator.vibrate(VIBRATION_PATTERN, VIBRATION_PATTERN_REPEAT,
- VIBRATION_ATTRIBUTES);
- mIsVibrating = true;
- }
- } else if (foregroundCall != null) {
- // The first incoming call added to Telecom is not a foreground call at this point
- // in time. If the current foreground call is null at point, don't play call-waiting
- // as the call will eventually be promoted to the foreground call and play the
- // ring tone.
- Log.v(this, "Playing call-waiting tone.");
-
- // All incoming calls are in background so play call waiting.
- stopRinging(call, "Stop for call-waiting");
-
-
- if (mState != STATE_CALL_WAITING) {
- Log.event(call, Log.Events.START_CALL_WAITING_TONE);
- mState = STATE_CALL_WAITING;
- }
-
- if (mCallWaitingPlayer == null) {
- mCallWaitingPlayer =
- mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
- mCallWaitingPlayer.startTone();
- }
+ mCallWaitingPlayer.stopTone();
+ mCallWaitingPlayer = null;
}
}
@@ -251,37 +194,6 @@
return manager.matchesCallFilter(extras);
}
- private void stopRinging(Call call, String reasonTag) {
- if (mState == STATE_RINGING) {
- Log.event(call, Log.Events.STOP_RINGER, reasonTag);
- mState = STATE_STOPPED;
- }
-
- mRingtonePlayer.stop();
-
- if (mIsVibrating) {
- mVibrator.cancel();
- mIsVibrating = false;
- }
-
- // Even though stop is asynchronous it's ok to update the audio manager. Things like audio
- // focus are voluntary so releasing focus too early is not detrimental.
- mCallAudioManager.setIsRinging(call, false);
- }
-
- private void stopCallWaiting(Call call) {
- Log.v(this, "stop call waiting.");
- if (mCallWaitingPlayer != null) {
- mCallWaitingPlayer.stopTone();
- mCallWaitingPlayer = null;
- }
-
- if (mState == STATE_CALL_WAITING) {
- Log.event(call, Log.Events.STOP_CALL_WAITING_TONE);
- mState = STATE_STOPPED;
- }
- }
-
private boolean shouldVibrate(Context context) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int ringerMode = audioManager.getRingerModeInternal();
@@ -296,7 +208,6 @@
if (!mVibrator.hasVibrator()) {
return false;
}
- return Settings.System.getInt(context.getContentResolver(),
- Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+ return mSystemSettingsUtil.canVibrateWhenRinging(context);
}
}
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
new file mode 100644
index 0000000..5044a90
--- /dev/null
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.media.AudioManager;
+import android.media.RingtoneManager;
+import android.media.Ringtone;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Uses a Uri to obtain a {@link Ringtone} from the {@link RingtoneManager} that can be played
+ * by the system during an incoming call.
+ */
+@VisibleForTesting
+public class RingtoneFactory {
+
+ private final Context mContext;
+
+ public RingtoneFactory(Context context) {
+ mContext = context;
+ }
+
+ public Ringtone getRingtone(Uri ringtoneUri) {
+ if (ringtoneUri == null) {
+ ringtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
+ }
+
+ Ringtone ringtone = RingtoneManager.getRingtone(mContext, ringtoneUri);
+ if (ringtone != null) {
+ ringtone.setStreamType(AudioManager.STREAM_RING);
+ }
+ return ringtone;
+ }
+}
diff --git a/src/com/android/server/telecom/Runnable.java b/src/com/android/server/telecom/Runnable.java
new file mode 100644
index 0000000..ce5b6d2
--- /dev/null
+++ b/src/com/android/server/telecom/Runnable.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+/**
+ * Encapsulates session logging in a Runnable to reduce code duplication when continuing subsessions
+ * in a handler/thread.
+ */
+public abstract class Runnable {
+
+ private Session mSubsession;
+ private final String mSubsessionName;
+ private Object mLock = new Object();
+ private final java.lang.Runnable mRunnable = new java.lang.Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ try {
+ Log.continueSession(mSubsession, mSubsessionName);
+ loggedRun();
+ } finally {
+ Log.endSession();
+ mSubsession = null;
+ }
+ }
+ }
+ };
+
+ public Runnable(String subsessionName) {
+ mSubsessionName = subsessionName;
+ }
+
+ /**
+ * Return the runnable that will be canceled in the handler queue.
+ * @return Runnable object to cancel.
+ */
+ public final java.lang.Runnable getRunnableToCancel() {
+ return mRunnable;
+ }
+
+ /**
+ * Creates a Runnable and a logging subsession that can be used in a handler/thread. Be sure to
+ * call cancel() if this session is never going to be run (removed from a handler queue, for
+ * for example).
+ * @return A Java Runnable that can be used in a handler queue or thread.
+ */
+ public java.lang.Runnable prepare() {
+ cancel();
+ mSubsession = Log.createSubsession();
+ return mRunnable;
+ }
+
+ /**
+ * This method is used to clean up the active session if the Runnable gets removed from a
+ * handler and is never run.
+ */
+ public void cancel() {
+ synchronized (mLock) {
+ Log.cancelSubsession(mSubsession);
+ mSubsession = null;
+ }
+ }
+
+ /**
+ * The method that will be run in the handler/thread.
+ */
+ abstract public void loggedRun();
+
+}
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 2e63512..18d6581 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -25,6 +25,7 @@
import android.text.TextUtils;
import android.util.ArraySet;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.Collections;
@@ -114,36 +115,46 @@
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
- synchronized (mLock) {
- Log.i(this, "Service bound %s", componentName);
+ try {
+ Log.startSession("SBC.oSC");
+ synchronized (mLock) {
+ Log.i(this, "Service bound %s", componentName);
- Log.event(mCall, Log.Events.CS_BOUND, componentName);
- mCall = null;
+ Log.event(mCall, Log.Events.CS_BOUND, componentName);
+ mCall = null;
- // Unbind request was queued so unbind immediately.
- if (mIsBindingAborted) {
- clearAbort();
- logServiceDisconnected("onServiceConnected");
- mContext.unbindService(this);
- handleFailedConnection();
- return;
+ // Unbind request was queued so unbind immediately.
+ if (mIsBindingAborted) {
+ clearAbort();
+ logServiceDisconnected("onServiceConnected");
+ mContext.unbindService(this);
+ handleFailedConnection();
+ return;
+ }
+
+ mServiceConnection = this;
+ setBinder(binder);
+ handleSuccessfulConnection();
}
-
- mServiceConnection = this;
- setBinder(binder);
- handleSuccessfulConnection();
+ } finally {
+ Log.endSession();
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
- synchronized (mLock) {
- logServiceDisconnected("onServiceDisconnected");
+ try {
+ Log.startSession("SBC.oSD");
+ synchronized (mLock) {
+ logServiceDisconnected("onServiceDisconnected");
- mServiceConnection = null;
- clearAbort();
+ mServiceConnection = null;
+ clearAbort();
- handleServiceDisconnected();
+ handleServiceDisconnected();
+ }
+ } finally {
+ Log.endSession();
}
}
}
@@ -253,7 +264,8 @@
return mComponentName;
}
- final boolean isServiceValid(String actionName) {
+ @VisibleForTesting
+ public boolean isServiceValid(String actionName) {
if (mBinder == null) {
Log.w(this, "%s invoked while service is unbound", actionName);
return false;
diff --git a/src/com/android/server/telecom/Session.java b/src/com/android/server/telecom/Session.java
new file mode 100644
index 0000000..51ef0fa
--- /dev/null
+++ b/src/com/android/server/telecom/Session.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.NonNull;
+
+import java.util.ArrayList;
+
+/**
+ * The session that stores information about a thread's point of entry into the Telecom code that
+ * persists until the thread exits Telecom.
+ */
+public class Session {
+
+ public static final String START_SESSION = "START_SESSION";
+ public static final String CREATE_SUBSESSION = "CREATE_SUBSESSION";
+ public static final String CONTINUE_SUBSESSION = "CONTINUE_SUBSESSION";
+ public static final String END_SUBSESSION = "END_SUBSESSION";
+ public static final String END_SESSION = "END_SESSION";
+
+ public static final int UNDEFINED = -1;
+
+ private String mSessionId;
+ private String mShortMethodName;
+ private long mExecutionStartTimeMs;
+ private long mExecutionEndTimeMs = UNDEFINED;
+ private Session mParentSession;
+ private ArrayList<Session> mChildSessions;
+ private boolean mIsCompleted = false;
+ private int mChildCounter = 0;
+ // True if this is a subsession that has been started from the same thread as the parent
+ // session. This can happen if Log.startSession(...) is called multiple times on the same
+ // thread in the case of one Telecom entry point method calling another entry point method.
+ // In this case, we can just make this subsession "invisible," but still keep track of it so
+ // that the Log.endSession() calls match up.
+ private boolean mIsStartedFromActiveSession = false;
+ // Optionally provided info about the method/class/component that started the session in order
+ // to make Logging easier. This info will be provided in parentheses along with the session.
+ private String mOwnerInfo;
+
+ public Session(String sessionId, String shortMethodName, long startTimeMs, long threadID,
+ boolean isStartedFromActiveSession, String ownerInfo) {
+ setSessionId(sessionId);
+ setShortMethodName(shortMethodName);
+ mExecutionStartTimeMs = startTimeMs;
+ mParentSession = null;
+ mChildSessions = new ArrayList<>(5);
+ mIsStartedFromActiveSession = isStartedFromActiveSession;
+ mOwnerInfo = ownerInfo;
+ }
+
+ public void setSessionId(@NonNull String sessionId) {
+ if(sessionId == null) {
+ mSessionId = "?";
+ }
+ mSessionId = sessionId;
+ }
+
+ public String getShortMethodName() {
+ return mShortMethodName;
+ }
+
+ public void setShortMethodName(String shortMethodName) {
+ if(shortMethodName == null) {
+ shortMethodName = "";
+ }
+ mShortMethodName = shortMethodName;
+ }
+
+ public void setParentSession(Session parentSession) {
+ mParentSession = parentSession;
+ }
+
+ public void addChild(Session childSession) {
+ if(childSession != null) {
+ mChildSessions.add(childSession);
+ }
+ }
+
+ public void removeChild(Session child) {
+ if(child != null) {
+ mChildSessions.remove(child);
+ }
+ }
+
+ public long getExecutionStartTimeMilliseconds() {
+ return mExecutionStartTimeMs;
+ }
+
+ public void setExecutionStartTimeMs(long startTimeMs) {
+ mExecutionStartTimeMs = startTimeMs;
+ }
+
+ public Session getParentSession() {
+ return mParentSession;
+ }
+
+ public ArrayList<Session> getChildSessions() {
+ return mChildSessions;
+ }
+
+ public boolean isSessionCompleted() {
+ return mIsCompleted;
+ }
+
+ public boolean isStartedFromActiveSession() {
+ return mIsStartedFromActiveSession;
+ }
+
+ // Mark this session complete. This will be deleted by Log when all subsessions are complete
+ // as well.
+ public void markSessionCompleted(long executionEndTimeMs) {
+ mExecutionEndTimeMs = executionEndTimeMs;
+ mIsCompleted = true;
+ }
+
+ public long getLocalExecutionTime() {
+ if(mExecutionEndTimeMs == UNDEFINED) {
+ return UNDEFINED;
+ }
+ return mExecutionEndTimeMs - mExecutionStartTimeMs;
+ }
+
+ public synchronized String getNextChildId() {
+ return String.valueOf(mChildCounter++);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Session)) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ Session otherSession = (Session) obj;
+ return (mSessionId.equals(otherSession.mSessionId)) &&
+ (mShortMethodName.equals(otherSession.mShortMethodName)) &&
+ mExecutionStartTimeMs == otherSession.mExecutionStartTimeMs &&
+ mParentSession == otherSession.mParentSession &&
+ mChildSessions.equals(otherSession.mChildSessions) &&
+ mIsCompleted == otherSession.mIsCompleted &&
+ mExecutionEndTimeMs == otherSession.mExecutionEndTimeMs &&
+ mChildCounter == otherSession.mChildCounter &&
+ mIsStartedFromActiveSession == otherSession.mIsStartedFromActiveSession &&
+ mOwnerInfo == otherSession.mOwnerInfo;
+ }
+
+ // Builds full session id recursively
+ private String getFullSessionId() {
+ // Cache mParentSession locally to prevent a concurrency problem where
+ // Log.endParentSessions() is called while a logging statement is running (Log.i, for
+ // example) and setting mParentSession to null in a different thread after the null check
+ // occurred.
+ Session parentSession = mParentSession;
+ if(parentSession == null) {
+ return mSessionId;
+ } else {
+ return parentSession.getFullSessionId() + "_" + mSessionId;
+ }
+ }
+
+ // Print out the full Session tree from any subsession node
+ public String printFullSessionTree() {
+ // Get to the top of the tree
+ Session topNode = this;
+ while(topNode.getParentSession() != null) {
+ topNode = topNode.getParentSession();
+ }
+ return topNode.printSessionTree();
+ }
+
+ // Recursively move down session tree using DFS, but print out each node when it is reached.
+ public String printSessionTree() {
+ StringBuilder sb = new StringBuilder();
+ printSessionTree(0, sb);
+ return sb.toString();
+ }
+
+ private void printSessionTree(int tabI, StringBuilder sb) {
+ sb.append(toString());
+ for (Session child : mChildSessions) {
+ sb.append("\n");
+ for(int i = 0; i <= tabI; i++) {
+ sb.append("\t");
+ }
+ child.printSessionTree(tabI + 1, sb);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if(mParentSession != null && mIsStartedFromActiveSession) {
+ // Log.startSession was called from within another active session. Use the parent's
+ // Id instead of the child to reduce confusion.
+ return mParentSession.toString();
+ } else {
+ StringBuilder methodName = new StringBuilder();
+ methodName.append(mShortMethodName);
+ if(mOwnerInfo != null && !mOwnerInfo.isEmpty()) {
+ methodName.append("(InCall package: ");
+ methodName.append(mOwnerInfo);
+ methodName.append(")");
+ }
+ return methodName.toString() + "@" + getFullSessionId();
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/StatusBarNotifier.java b/src/com/android/server/telecom/StatusBarNotifier.java
index c8c3c18..d8ede59 100644
--- a/src/com/android/server/telecom/StatusBarNotifier.java
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -19,12 +19,15 @@
import android.app.StatusBarManager;
import android.content.Context;
+import com.android.internal.annotations.VisibleForTesting;
+
// TODO: Needed for move to system service: import com.android.internal.R;
/**
* Manages the special status bar notifications used by the phone app.
*/
-final class StatusBarNotifier extends CallsManagerListenerBase {
+@VisibleForTesting
+public class StatusBarNotifier extends CallsManagerListenerBase {
private static final String SLOT_MUTE = "mute";
private static final String SLOT_SPEAKERPHONE = "speakerphone";
@@ -50,7 +53,8 @@
}
}
- void notifyMute(boolean isMuted) {
+ @VisibleForTesting
+ public void notifyMute(boolean isMuted) {
// Never display anything if there are no calls.
if (!mCallsManager.hasAnyCalls()) {
isMuted = false;
@@ -74,7 +78,8 @@
mIsShowingMute = isMuted;
}
- void notifySpeakerphone(boolean isSpeakerphone) {
+ @VisibleForTesting
+ public void notifySpeakerphone(boolean isSpeakerphone) {
// Never display anything if there are no calls.
if (!mCallsManager.hasAnyCalls()) {
isSpeakerphone = false;
diff --git a/src/com/android/server/telecom/SystemLoggingContainer.java b/src/com/android/server/telecom/SystemLoggingContainer.java
new file mode 100644
index 0000000..0b65b09
--- /dev/null
+++ b/src/com/android/server/telecom/SystemLoggingContainer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+/**
+ * Create a container for the Android Logging class so that it can be mocked in testing.
+ */
+public class SystemLoggingContainer {
+
+ public void v(String TAG, String msg) {
+ android.util.Slog.v(TAG, msg);
+ }
+
+ public void d(String TAG, String msg) {
+ android.util.Slog.d(TAG, msg);
+ }
+
+ public void i(String TAG, String msg) {
+ android.util.Slog.i(TAG, msg);
+ }
+
+ public void w(String TAG, String msg) {
+ android.util.Slog.w(TAG, msg);
+ }
+
+ public void e(String TAG, String msg, Throwable tr) {
+ android.util.Slog.e(TAG, msg, tr);
+ }
+
+ public void wtf(String TAG, String msg, Throwable tr) {
+ android.util.Slog.wtf(TAG, msg, tr);
+ }
+}
diff --git a/src/com/android/server/telecom/SystemSettingsUtil.java b/src/com/android/server/telecom/SystemSettingsUtil.java
new file mode 100644
index 0000000..3c75e4d
--- /dev/null
+++ b/src/com/android/server/telecom/SystemSettingsUtil.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.provider.Settings;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Accesses the Global System settings for more control during testing.
+ */
+@VisibleForTesting
+public class SystemSettingsUtil {
+
+ public boolean isTheaterModeOn(Context context) {
+ return Settings.Global.getInt(context.getContentResolver(), Settings.Global.THEATER_MODE_ON,
+ 0) == 1;
+ }
+
+ public boolean canVibrateWhenRinging(Context context) {
+ return Settings.System.getInt(context.getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+ }
+}
diff --git a/src/com/android/server/telecom/SystemStateProvider.java b/src/com/android/server/telecom/SystemStateProvider.java
new file mode 100644
index 0000000..0b636cf
--- /dev/null
+++ b/src/com/android/server/telecom/SystemStateProvider.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Provides various system states to the rest of the telecom codebase. So far, that's only car-mode.
+ */
+public class SystemStateProvider {
+
+ public static interface SystemStateListener {
+ public void onCarModeChanged(boolean isCarMode);
+ }
+
+ private final Context mContext;
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.startSession("SSP.oR");
+ try {
+ String action = intent.getAction();
+ if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(action)) {
+ onEnterCarMode();
+ } else if (UiModeManager.ACTION_EXIT_CAR_MODE.equals(action)) {
+ onExitCarMode();
+ } else {
+ Log.w(this, "Unexpected intent received: %s", intent.getAction());
+ }
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+
+ private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>();
+ private boolean mIsCarMode;
+
+ public SystemStateProvider(Context context) {
+ mContext = context;
+
+ IntentFilter intentFilter = new IntentFilter(UiModeManager.ACTION_ENTER_CAR_MODE);
+ intentFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE);
+ mContext.registerReceiver(mBroadcastReceiver, intentFilter);
+ Log.i(this, "Registering car mode receiver: %s", intentFilter);
+
+ mIsCarMode = getSystemCarMode();
+ }
+
+ public void addListener(SystemStateListener listener) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ }
+
+ public boolean removeListener(SystemStateListener listener) {
+ return mListeners.remove(listener);
+ }
+
+ public boolean isCarMode() {
+ return mIsCarMode;
+ }
+
+ private void onEnterCarMode() {
+ if (!mIsCarMode) {
+ Log.i(this, "Entering carmode");
+ mIsCarMode = true;
+ notifyCarMode();
+ }
+ }
+
+ private void onExitCarMode() {
+ if (mIsCarMode) {
+ Log.i(this, "Exiting carmode");
+ mIsCarMode = false;
+ notifyCarMode();
+ }
+ }
+
+ private void notifyCarMode() {
+ for (SystemStateListener listener : mListeners) {
+ listener.onCarModeChanged(mIsCarMode);
+ }
+ }
+
+ /**
+ * Checks the system for the current car mode.
+ *
+ * @return True if in car mode, false otherwise.
+ */
+ private boolean getSystemCarMode() {
+ UiModeManager uiModeManager =
+ (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
+
+ if (uiModeManager != null) {
+ return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index 89aa2aa..c5db6de 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -33,6 +33,8 @@
public static final String ACTION_CLEAR_MISSED_CALLS =
"com.android.server.telecom.ACTION_CLEAR_MISSED_CALLS";
+ public static final String EXTRA_USERHANDLE = "userhandle";
+
private final Context mContext;
private final CallsManager mCallsManager;
@@ -45,6 +47,11 @@
String action = intent.getAction();
Log.v(this, "Action received: %s.", action);
+ UserHandle userHandle = intent.getParcelableExtra(EXTRA_USERHANDLE);
+ if (userHandle == null) {
+ Log.d(this, "user handle can't be null, not processing the broadcast");
+ return;
+ }
MissedCallNotifier missedCallNotifier = mCallsManager.getMissedCallNotifier();
@@ -52,26 +59,26 @@
if (ACTION_SEND_SMS_FROM_NOTIFICATION.equals(action)) {
// Close the notification shade and the notification itself.
closeSystemDialogs(mContext);
- missedCallNotifier.clearMissedCalls();
+ missedCallNotifier.clearMissedCalls(userHandle);
Intent callIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivityAsUser(callIntent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(callIntent, userHandle);
// Call back recent caller from the missed call notification.
} else if (ACTION_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
// Close the notification shade and the notification itself.
closeSystemDialogs(mContext);
- missedCallNotifier.clearMissedCalls();
+ missedCallNotifier.clearMissedCalls(userHandle);
- Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
+ Intent callIntent = new Intent(Intent.ACTION_CALL, intent.getData());
callIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- mContext.startActivityAsUser(callIntent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(callIntent, userHandle);
// Clear the missed call notification and call log entries.
} else if (ACTION_CLEAR_MISSED_CALLS.equals(action)) {
- missedCallNotifier.clearMissedCalls();
+ missedCallNotifier.clearMissedCalls(userHandle);
}
}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 9b0c7c7..a269085 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -17,6 +17,7 @@
package com.android.server.telecom;
import static android.Manifest.permission.CALL_PHONE;
+import static android.Manifest.permission.DUMP;
import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.Manifest.permission.READ_PHONE_STATE;
import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
@@ -34,26 +35,27 @@
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
-import android.os.IBinder;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.DefaultDialerManager;
+import android.telecom.ParcelableCallAnalytics;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
-import android.text.TextUtils;
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -61,6 +63,40 @@
* Implementation of the ITelecom interface.
*/
public class TelecomServiceImpl {
+ public interface DefaultDialerManagerAdapter {
+ String getDefaultDialerApplication(Context context);
+ boolean setDefaultDialerApplication(Context context, String packageName);
+ boolean isDefaultOrSystemDialer(Context context, String packageName);
+ }
+
+ static class DefaultDialerManagerAdapterImpl implements DefaultDialerManagerAdapter {
+ @Override
+ public String getDefaultDialerApplication(Context context) {
+ return DefaultDialerManager.getDefaultDialerApplication(context);
+ }
+
+ @Override
+ public boolean setDefaultDialerApplication(Context context, String packageName) {
+ return DefaultDialerManager.setDefaultDialerApplication(context, packageName);
+ }
+
+ @Override
+ public boolean isDefaultOrSystemDialer(Context context, String packageName) {
+ return DefaultDialerManager.isDefaultOrSystemDialer(context, packageName);
+ }
+ }
+
+ public interface SubscriptionManagerAdapter {
+ int getDefaultVoiceSubId();
+ }
+
+ static class SubscriptionManagerAdapterImpl implements SubscriptionManagerAdapter {
+ @Override
+ public int getDefaultVoiceSubId() {
+ return SubscriptionManager.getDefaultVoiceSubscriptionId();
+ }
+ }
+
private static final String PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION =
"android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION";
private static final int DEFAULT_VIDEO_STATE = -1;
@@ -69,30 +105,27 @@
@Override
public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme,
String callingPackage) {
- synchronized (mLock) {
- if (!canReadPhoneState(callingPackage, "getDefaultOutgoingPhoneAccount")) {
- return null;
- }
-
- long token = Binder.clearCallingIdentity();
- try {
- PhoneAccountHandle defaultOutgoingPhoneAccount =
- mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(uriScheme);
- // Make sure that the calling user can see this phone account.
- // TODO: Does this isVisible check actually work considering we are clearing
- // the calling identity?
- if (defaultOutgoingPhoneAccount != null
- && !isVisibleToCaller(defaultOutgoingPhoneAccount)) {
- Log.w(this, "No account found for the calling user");
+ try {
+ Log.startSession("TSI.gDOPA");
+ synchronized (mLock) {
+ if (!canReadPhoneState(callingPackage, "getDefaultOutgoingPhoneAccount")) {
return null;
}
- return defaultOutgoingPhoneAccount;
- } catch (Exception e) {
- Log.e(this, e, "getDefaultOutgoingPhoneAccount");
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
+
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ return mPhoneAccountRegistrar
+ .getOutgoingPhoneAccountForScheme(uriScheme, callingUserHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "getDefaultOutgoingPhoneAccount");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@@ -100,94 +133,109 @@
public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
synchronized (mLock) {
try {
- PhoneAccountHandle userSelectedOutgoingPhoneAccount =
- mPhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount();
- // Make sure that the calling user can see this phone account.
- if (!isVisibleToCaller(userSelectedOutgoingPhoneAccount)) {
- Log.w(this, "No account found for the calling user");
- return null;
- }
- return userSelectedOutgoingPhoneAccount;
+ Log.startSession("TSI.gUSOPA");
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ return mPhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount(
+ callingUserHandle);
} catch (Exception e) {
Log.e(this, e, "getUserSelectedOutgoingPhoneAccount");
throw e;
+ } finally {
+ Log.endSession();
}
}
}
@Override
public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
- synchronized (mLock) {
- enforceModifyPermission();
-
- long token = Binder.clearCallingIdentity();
- try {
- mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(accountHandle);
- } catch (Exception e) {
- Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Log.startSession("TSI.sUSOPA");
+ synchronized (mLock) {
+ enforceModifyPermission();
+ UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(
+ accountHandle, callingUserHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@Override
public List<PhoneAccountHandle> getCallCapablePhoneAccounts(
boolean includeDisabledAccounts, String callingPackage) {
- if (!canReadPhoneState(callingPackage, "getDefaultOutgoingPhoneAccount")) {
- return Collections.emptyList();
- }
-
- synchronized (mLock) {
- long token = Binder.clearCallingIdentity();
- try {
- // TODO: Does this isVisible check actually work considering we are clearing
- // the calling identity?
- return filterForAccountsVisibleToCaller(
- mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
- null, includeDisabledAccounts));
- } catch (Exception e) {
- Log.e(this, e, "getCallCapablePhoneAccounts");
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Log.startSession("TSI.gCCPA");
+ if (!canReadPhoneState(callingPackage, "getDefaultOutgoingPhoneAccount")) {
+ return Collections.emptyList();
}
+ synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ return mPhoneAccountRegistrar.getCallCapablePhoneAccounts(null,
+ includeDisabledAccounts, callingUserHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "getCallCapablePhoneAccounts");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } finally {
+ Log.endSession();
}
}
@Override
public List<PhoneAccountHandle> getPhoneAccountsSupportingScheme(String uriScheme,
String callingPackage) {
- synchronized (mLock) {
- if (!canReadPhoneState(callingPackage, "getPhoneAccountsSupportingScheme")) {
- return Collections.emptyList();
+ try {
+ Log.startSession("TSI.gPASS");
+ synchronized (mLock) {
+ if (!canReadPhoneState(callingPackage, "getPhoneAccountsSupportingScheme")) {
+ return Collections.emptyList();
+ }
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ return mPhoneAccountRegistrar.getCallCapablePhoneAccounts(uriScheme, false,
+ callingUserHandle);
+ } catch (Exception e) {
+ Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme);
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
-
- long token = Binder.clearCallingIdentity();
- try {
- // TODO: Does this isVisible check actually work considering we are clearing
- // the calling identity?
- return filterForAccountsVisibleToCaller(
- mPhoneAccountRegistrar.getCallCapablePhoneAccounts(uriScheme, false));
- } catch (Exception e) {
- Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme);
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ } finally {
+ Log.endSession();
}
}
@Override
public List<PhoneAccountHandle> getPhoneAccountsForPackage(String packageName) {
synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
try {
- return filterForAccountsVisibleToCaller(
- mPhoneAccountRegistrar.getPhoneAccountsForPackage(packageName));
+ Log.startSession("TSI.gPAFP");
+ return mPhoneAccountRegistrar.getPhoneAccountsForPackage(packageName,
+ callingUserHandle);
} catch (Exception e) {
Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -195,16 +243,23 @@
@Override
public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle) {
synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
try {
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [gPA]", accountHandle);
- return null;
- }
- // TODO: Do we really want to return for *any* user?
- return mPhoneAccountRegistrar.getPhoneAccount(accountHandle);
+ 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();
}
}
}
@@ -213,11 +268,14 @@
public int getAllPhoneAccountsCount() {
synchronized (mLock) {
try {
+ Log.startSession("TSI.gAPAC");
// This list is pre-filtered for the calling user.
return getAllPhoneAccounts().size();
} catch (Exception e) {
Log.e(this, e, "getAllPhoneAccountsCount");
throw e;
+ } finally {
+ Log.endSession();
}
}
}
@@ -225,20 +283,17 @@
@Override
public List<PhoneAccount> getAllPhoneAccounts() {
synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
try {
- List<PhoneAccount> allPhoneAccounts = mPhoneAccountRegistrar
- .getAllPhoneAccounts();
- List<PhoneAccount> profilePhoneAccounts = new ArrayList<>(
- allPhoneAccounts.size());
- for (PhoneAccount phoneAccount : allPhoneAccounts) {
- if (isVisibleToCaller(phoneAccount)) {
- profilePhoneAccounts.add(phoneAccount);
- }
- }
- return profilePhoneAccounts;
+ Log.startSession("TSI.gAPA");
+ return mPhoneAccountRegistrar.getAllPhoneAccounts(callingUserHandle);
} catch (Exception e) {
Log.e(this, e, "getAllPhoneAccounts");
throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@@ -246,92 +301,104 @@
@Override
public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
try {
- return filterForAccountsVisibleToCaller(
- mPhoneAccountRegistrar.getAllPhoneAccountHandles());
+ Log.startSession("TSI.gAPAH");
+ return mPhoneAccountRegistrar.getAllPhoneAccountHandles(callingUserHandle);
} catch (Exception e) {
Log.e(this, e, "getAllPhoneAccounts");
throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
}
@Override
public PhoneAccountHandle getSimCallManager() {
- long token = Binder.clearCallingIdentity();
- int user;
try {
- user = ActivityManager.getCurrentUser();
+ Log.startSession("TSI.gSCM");
+ long token = Binder.clearCallingIdentity();
+ int user;
+ try {
+ user = ActivityManager.getCurrentUser();
+ return getSimCallManagerForUser(user);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
- return getSimCallManagerForUser(user);
}
@Override
public PhoneAccountHandle getSimCallManagerForUser(int user) {
synchronized (mLock) {
try {
- PhoneAccountHandle accountHandle = null;
-
+ Log.startSession("TSI.gSCMFU");
+ final int callingUid = Binder.getCallingUid();
long token = Binder.clearCallingIdentity();
try {
- accountHandle = mPhoneAccountRegistrar.getSimCallManager(user);
+ if (user != ActivityManager.getCurrentUser()) {
+ enforceCrossUserPermission(callingUid);
+ }
+ return mPhoneAccountRegistrar.getSimCallManager(UserHandle.of(user));
} finally {
- // We restore early so that isVisibleToCaller invocation below uses the
- // right user context.
Binder.restoreCallingIdentity(token);
}
-
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [gsCM]", accountHandle);
- return null;
- }
- return accountHandle;
} catch (Exception e) {
Log.e(this, e, "getSimCallManager");
throw e;
+ } finally {
+ Log.endSession();
}
}
}
@Override
public void registerPhoneAccount(PhoneAccount account) {
- synchronized (mLock) {
- if (!mContext.getApplicationContext().getResources().getBoolean(
- com.android.internal.R.bool.config_voice_capable)) {
- Log.w(this, "registerPhoneAccount not allowed on non-voice capable device.");
- return;
- }
- try {
- enforcePhoneAccountModificationForPackage(
- account.getAccountHandle().getComponentName().getPackageName());
- if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
- enforceRegisterSimSubscriptionPermission();
+ try {
+ Log.startSession("TSI.rPA");
+ synchronized (mLock) {
+ if (!mContext.getApplicationContext().getResources().getBoolean(
+ com.android.internal.R.bool.config_voice_capable)) {
+ Log.w(this,
+ "registerPhoneAccount not allowed on non-voice capable device.");
+ return;
}
- if (account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
- enforceRegisterMultiUser();
- }
- enforceUserHandleMatchesCaller(account.getAccountHandle());
-
- mPhoneAccountRegistrar.registerPhoneAccount(account);
-
- // Broadcast an intent indicating the phone account which was registered.
- long token = Binder.clearCallingIdentity();
try {
- Intent intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED);
- intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
- account.getAccountHandle());
- Log.i(this, "Sending phone-account registered intent as user");
- mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
- PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION);
- } finally {
- Binder.restoreCallingIdentity(token);
+ enforcePhoneAccountModificationForPackage(
+ account.getAccountHandle().getComponentName().getPackageName());
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ enforceRegisterSimSubscriptionPermission();
+ }
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
+ enforceRegisterMultiUser();
+ }
+ enforceUserHandleMatchesCaller(account.getAccountHandle());
+ mPhoneAccountRegistrar.registerPhoneAccount(account);
+ // Broadcast an intent indicating the phone account which was registered.
+ long token = Binder.clearCallingIdentity();
+ try {
+ Intent intent = new Intent(
+ TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED);
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ account.getAccountHandle());
+ Log.i(this, "Sending phone-account registered intent as user");
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
+ PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } catch (Exception e) {
+ Log.e(this, e, "registerPhoneAccount %s", account);
+ throw e;
}
- } catch (Exception e) {
- Log.e(this, e, "registerPhoneAccount %s", account);
- throw e;
}
+ } finally {
+ Log.endSession();
}
}
@@ -339,6 +406,7 @@
public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
synchronized (mLock) {
try {
+ Log.startSession("TSI.uPA");
enforcePhoneAccountModificationForPackage(
accountHandle.getComponentName().getPackageName());
enforceUserHandleMatchesCaller(accountHandle);
@@ -349,7 +417,8 @@
try {
Intent intent =
new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED);
- intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
+ intent.putExtra(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
Log.i(this, "Sending phone-account unregistered intent as user");
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION);
@@ -359,6 +428,8 @@
} catch (Exception e) {
Log.e(this, e, "unregisterPhoneAccount %s", accountHandle);
throw e;
+ } finally {
+ Log.endSession();
}
}
}
@@ -367,12 +438,15 @@
public void clearAccounts(String packageName) {
synchronized (mLock) {
try {
+ Log.startSession("TSI.cA");
enforcePhoneAccountModificationForPackage(packageName);
mPhoneAccountRegistrar
.clearAccounts(packageName, Binder.getCallingUserHandle());
} catch (Exception e) {
Log.e(this, e, "clearAccounts %s", packageName);
throw e;
+ } finally {
+ Log.endSession();
}
}
}
@@ -383,25 +457,30 @@
@Override
public boolean isVoiceMailNumber(PhoneAccountHandle accountHandle, String number,
String callingPackage) {
- synchronized (mLock) {
- if (!canReadPhoneState(callingPackage, "isVoiceMailNumber")) {
- return false;
+ try {
+ Log.startSession("TSI.iVMN");
+ synchronized (mLock) {
+ if (!canReadPhoneState(callingPackage, "isVoiceMailNumber")) {
+ return false;
+ }
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ if (!isPhoneAccountHandleVisibleToCallingUser(accountHandle,
+ callingUserHandle)) {
+ Log.d(this, "%s is not visible for the calling user [iVMN]", accountHandle);
+ return false;
+ }
+ long token = Binder.clearCallingIdentity();
+ try {
+ return mPhoneAccountRegistrar.isVoiceMailNumber(accountHandle, number);
+ } catch (Exception e) {
+ Log.e(this, e, "getSubscriptionIdForPhoneAccount");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
-
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [iVMN]", accountHandle);
- return false;
- }
-
- long token = Binder.clearCallingIdentity();
- try {
- return mPhoneAccountRegistrar.isVoiceMailNumber(accountHandle, number);
- } catch (Exception e) {
- Log.e(this, e, "getSubscriptionIdForPhoneAccount");
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ } finally {
+ Log.endSession();
}
}
@@ -410,27 +489,33 @@
*/
@Override
public String getVoiceMailNumber(PhoneAccountHandle accountHandle, String callingPackage) {
- synchronized (mLock) {
- if (!canReadPhoneState(callingPackage, "getVoiceMailNumber")) {
- return null;
- }
-
- try {
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [gVMN]", accountHandle);
+ try {
+ Log.startSession("TSI.gVMN");
+ synchronized (mLock) {
+ if (!canReadPhoneState(callingPackage, "getVoiceMailNumber")) {
return null;
}
-
- int subId = SubscriptionManager.getDefaultVoiceSubscriptionId();
- if (accountHandle != null) {
- subId = mPhoneAccountRegistrar
- .getSubscriptionIdForPhoneAccount(accountHandle);
+ try {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ if (!isPhoneAccountHandleVisibleToCallingUser(accountHandle,
+ callingUserHandle)) {
+ Log.d(this, "%s is not visible for the calling user [gVMN]",
+ accountHandle);
+ return null;
+ }
+ int subId = mSubscriptionManagerAdapter.getDefaultVoiceSubId();
+ if (accountHandle != null) {
+ subId = mPhoneAccountRegistrar
+ .getSubscriptionIdForPhoneAccount(accountHandle);
+ }
+ return getTelephonyManager().getVoiceMailNumber(subId);
+ } catch (Exception e) {
+ Log.e(this, e, "getSubscriptionIdForPhoneAccount");
+ throw e;
}
- return getTelephonyManager().getVoiceMailNumber(subId);
- } catch (Exception e) {
- Log.e(this, e, "getSubscriptionIdForPhoneAccount");
- throw e;
}
+ } finally {
+ Log.endSession();
}
}
@@ -439,27 +524,34 @@
*/
@Override
public String getLine1Number(PhoneAccountHandle accountHandle, String callingPackage) {
- if (!canReadPhoneState(callingPackage, "getLine1Number")) {
- return null;
- }
-
- synchronized (mLock) {
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [gL1N]", accountHandle);
+ try {
+ Log.startSession("getL1N");
+ if (!canReadPhoneState(callingPackage, "getLine1Number")) {
return null;
}
- long token = Binder.clearCallingIdentity();
- try {
- int subId =
- mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle);
- return getTelephonyManager().getLine1Number(subId);
- } catch (Exception e) {
- Log.e(this, e, "getSubscriptionIdForPhoneAccount");
- throw e;
- } finally {
- Binder.restoreCallingIdentity(token);
+ synchronized (mLock) {
+ final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ if (!isPhoneAccountHandleVisibleToCallingUser(accountHandle,
+ callingUserHandle)) {
+ Log.d(this, "%s is not visible for the calling user [gL1N]", accountHandle);
+ return null;
+ }
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ int subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
+ accountHandle);
+ return getTelephonyManager().getLine1Number(subId);
+ } catch (Exception e) {
+ Log.e(this, e, "getSubscriptionIdForPhoneAccount");
+ throw e;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@@ -468,16 +560,22 @@
*/
@Override
public void silenceRinger(String callingPackage) {
- synchronized (mLock) {
- enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
+ try {
+ Log.startSession("TSI.sR");
+ synchronized (mLock) {
+ enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
- long token = Binder.clearCallingIdentity();
- try {
- Log.i(this, "Silence Ringer requested by %s", callingPackage);
- mCallsManager.getRinger().silence();
- } finally {
- Binder.restoreCallingIdentity(token);
+ long token = Binder.clearCallingIdentity();
+ try {
+ Log.i(this, "Silence Ringer requested by %s", callingPackage);
+ mCallsManager.getCallAudioManager().silenceRingers();
+ mCallsManager.getInCallController().silenceRinger();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@@ -488,11 +586,16 @@
*/
@Override
public ComponentName getDefaultPhoneApp() {
- // No need to synchronize
- Resources resources = mContext.getResources();
- return new ComponentName(
- resources.getString(R.string.ui_default_package),
- resources.getString(R.string.dialer_default_class));
+ try {
+ Log.startSession("TSI.gDPA");
+ // No need to synchronize
+ Resources resources = mContext.getResources();
+ return new ComponentName(
+ resources.getString(R.string.ui_default_package),
+ resources.getString(R.string.dialer_default_class));
+ } finally {
+ Log.endSession();
+ }
}
/**
@@ -503,11 +606,16 @@
*/
@Override
public String getDefaultDialerPackage() {
- final long token = Binder.clearCallingIdentity();
try {
- return DefaultDialerManager.getDefaultDialerApplication(mContext);
+ Log.startSession("TSI.gDDP");
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return mDefaultDialerManagerAdapter.getDefaultDialerApplication(mContext);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
} finally {
- Binder.restoreCallingIdentity(token);
+ Log.endSession();
}
}
@@ -516,7 +624,12 @@
*/
@Override
public String getSystemDialerPackage() {
- return mContext.getResources().getString(R.string.ui_default_package);
+ try {
+ Log.startSession("TSI.gSDP");
+ return mContext.getResources().getString(R.string.ui_default_package);
+ } finally {
+ Log.endSession();
+ }
}
/**
@@ -524,14 +637,19 @@
*/
@Override
public boolean isInCall(String callingPackage) {
- if (!canReadPhoneState(callingPackage, "isInCall")) {
- return false;
- }
+ try {
+ Log.startSession("TSI.iIC");
+ if (!canReadPhoneState(callingPackage, "isInCall")) {
+ return false;
+ }
- synchronized (mLock) {
- final int callState = mCallsManager.getCallState();
- return callState == TelephonyManager.CALL_STATE_OFFHOOK
- || callState == TelephonyManager.CALL_STATE_RINGING;
+ synchronized (mLock) {
+ final int callState = mCallsManager.getCallState();
+ return callState == TelephonyManager.CALL_STATE_OFFHOOK
+ || callState == TelephonyManager.CALL_STATE_RINGING;
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -540,12 +658,22 @@
*/
@Override
public boolean isRinging(String callingPackage) {
- if (!canReadPhoneState(callingPackage, "isRinging")) {
- return false;
- }
+ try {
+ Log.startSession("TSI.iR");
+ if (!canReadPhoneState(callingPackage, "isRinging")) {
+ return false;
+ }
- synchronized (mLock) {
- return mCallsManager.getCallState() == TelephonyManager.CALL_STATE_RINGING;
+ synchronized (mLock) {
+ // Note: We are explicitly checking the calls telecom is tracking rather than
+ // relying on mCallsManager#getCallState(). Since getCallState() relies on the
+ // current state as tracked by PhoneStateBroadcaster, any failure to properly
+ // track the current call state there could result in the wrong ringing state
+ // being reported by this API.
+ return mCallsManager.hasRingingCall();
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -554,8 +682,13 @@
*/
@Override
public int getCallState() {
- synchronized (mLock) {
- return mCallsManager.getCallState();
+ try {
+ Log.startSession("TSI.getCallState");
+ synchronized (mLock) {
+ return mCallsManager.getCallState();
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -564,15 +697,20 @@
*/
@Override
public boolean endCall() {
- synchronized (mLock) {
- enforceModifyPermission();
+ try {
+ Log.startSession("TSI.eC");
+ synchronized (mLock) {
+ enforceModifyPermission();
- long token = Binder.clearCallingIdentity();
- try {
- return endCallInternal();
- } finally {
- Binder.restoreCallingIdentity(token);
+ long token = Binder.clearCallingIdentity();
+ try {
+ return endCallInternal();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@@ -581,15 +719,20 @@
*/
@Override
public void acceptRingingCall() {
- synchronized (mLock) {
- enforceModifyPermission();
+ try {
+ Log.startSession("TSI.aRC");
+ synchronized (mLock) {
+ enforceModifyPermission();
- long token = Binder.clearCallingIdentity();
- try {
- acceptRingingCallInternal(DEFAULT_VIDEO_STATE);
- } finally {
- Binder.restoreCallingIdentity(token);
+ long token = Binder.clearCallingIdentity();
+ try {
+ acceptRingingCallInternal(DEFAULT_VIDEO_STATE);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@@ -599,15 +742,20 @@
*/
@Override
public void acceptRingingCallWithVideoState(int videoState) {
- synchronized (mLock) {
- enforceModifyPermission();
+ try {
+ Log.startSession("TSI.aRCWVS");
+ synchronized (mLock) {
+ enforceModifyPermission();
- long token = Binder.clearCallingIdentity();
- try {
- acceptRingingCallInternal(videoState);
- } finally {
- Binder.restoreCallingIdentity(token);
+ long token = Binder.clearCallingIdentity();
+ try {
+ acceptRingingCallInternal(videoState);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@@ -616,18 +764,23 @@
*/
@Override
public void showInCallScreen(boolean showDialpad, String callingPackage) {
- if (!canReadPhoneState(callingPackage, "showInCallScreen")) {
- return;
- }
-
- synchronized (mLock) {
-
- long token = Binder.clearCallingIdentity();
- try {
- mCallsManager.getInCallController().bringToForeground(showDialpad);
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Log.startSession("TSI.sICS");
+ if (!canReadPhoneState(callingPackage, "showInCallScreen")) {
+ return;
}
+
+ synchronized (mLock) {
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ mCallsManager.getInCallController().bringToForeground(showDialpad);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -636,35 +789,46 @@
*/
@Override
public void cancelMissedCallsNotification(String callingPackage) {
- synchronized (mLock) {
- enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
- long token = Binder.clearCallingIdentity();
- try {
- mCallsManager.getMissedCallNotifier().clearMissedCalls();
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Log.startSession("TSI.cMCN");
+ synchronized (mLock) {
+ enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
+ UserHandle userHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ mCallsManager.getMissedCallNotifier().clearMissedCalls(userHandle);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
-
/**
* @see android.telecom.TelecomManager#handleMmi
*/
@Override
public boolean handlePinMmi(String dialString, String callingPackage) {
- synchronized (mLock) {
- enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
+ try {
+ Log.startSession("TSI.hPM");
+ synchronized (mLock) {
+ enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
- // Switch identity so that TelephonyManager checks Telecom's permissions instead.
- long token = Binder.clearCallingIdentity();
- boolean retval = false;
- try {
- retval = getTelephonyManager().handlePinMmi(dialString);
- } finally {
- Binder.restoreCallingIdentity(token);
+ // Switch identity so that TelephonyManager checks Telecom's permissions
+ // instead.
+ long token = Binder.clearCallingIdentity();
+ boolean retval = false;
+ try {
+ retval = getTelephonyManager().handlePinMmi(dialString);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ return retval;
}
-
- return retval;
+ }finally {
+ Log.endSession();
}
}
@@ -672,30 +836,35 @@
* @see android.telecom.TelecomManager#handleMmi
*/
@Override
- public boolean handlePinMmiForPhoneAccount(
- PhoneAccountHandle accountHandle,
- String dialString,
- String callingPackage) {
- synchronized (mLock) {
- enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
+ public boolean handlePinMmiForPhoneAccount(PhoneAccountHandle accountHandle,
+ String dialString, String callingPackage) {
+ try {
+ Log.startSession("TSI.hPMFPA");
+ synchronized (mLock) {
+ enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [hMMI]", accountHandle);
- return false;
+ UserHandle callingUserHandle = Binder.getCallingUserHandle();
+ if (!isPhoneAccountHandleVisibleToCallingUser(accountHandle,
+ callingUserHandle)) {
+ Log.d(this, "%s is not visible for the calling user [hMMI]", accountHandle);
+ return false;
+ }
+
+ // Switch identity so that TelephonyManager checks Telecom's permissions
+ // instead.
+ long token = Binder.clearCallingIdentity();
+ boolean retval = false;
+ try {
+ int subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
+ accountHandle);
+ retval = getTelephonyManager().handlePinMmiForSubscriber(subId, dialString);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return retval;
}
-
- // Switch identity so that TelephonyManager checks Telecom's permissions instead.
- long token = Binder.clearCallingIdentity();
- boolean retval = false;
- try {
- int subId = mPhoneAccountRegistrar
- .getSubscriptionIdForPhoneAccount(accountHandle);
- retval = getTelephonyManager().handlePinMmiForSubscriber(subId, dialString);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
-
- return retval;
+ }finally {
+ Log.endSession();
}
}
@@ -705,26 +874,32 @@
@Override
public Uri getAdnUriForPhoneAccount(PhoneAccountHandle accountHandle,
String callingPackage) {
- synchronized (mLock) {
- enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
+ try {
+ Log.startSession("TSI.aAUFPA");
+ synchronized (mLock) {
+ enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
+ if (!isPhoneAccountHandleVisibleToCallingUser(accountHandle,
+ Binder.getCallingUserHandle())) {
+ Log.d(this, "%s is not visible for the calling user [gA4PA]",
+ accountHandle);
+ return null;
+ }
+ // Switch identity so that TelephonyManager checks Telecom's permissions
+ // instead.
+ long token = Binder.clearCallingIdentity();
+ String retval = "content://icc/adn/";
+ try {
+ long subId = mPhoneAccountRegistrar
+ .getSubscriptionIdForPhoneAccount(accountHandle);
+ retval = retval + "subId/" + subId;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
- if (!isVisibleToCaller(accountHandle)) {
- Log.d(this, "%s is not visible for the calling user [gA4PA]", accountHandle);
- return null;
+ return Uri.parse(retval);
}
-
- // Switch identity so that TelephonyManager checks Telecom's permissions instead.
- long token = Binder.clearCallingIdentity();
- String retval = "content://icc/adn/";
- try {
- long subId = mPhoneAccountRegistrar
- .getSubscriptionIdForPhoneAccount(accountHandle);
- retval = retval + "subId/" + subId;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
-
- return Uri.parse(retval);
+ } finally {
+ Log.endSession();
}
}
@@ -733,12 +908,17 @@
*/
@Override
public boolean isTtySupported(String callingPackage) {
- if (!canReadPhoneState(callingPackage, "hasVoiceMailNumber")) {
- return false;
- }
+ try {
+ Log.startSession("TSI.iTS");
+ if (!canReadPhoneState(callingPackage, "hasVoiceMailNumber")) {
+ return false;
+ }
- synchronized (mLock) {
- return mCallsManager.isTtySupported();
+ synchronized (mLock) {
+ return mCallsManager.isTtySupported();
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -747,12 +927,17 @@
*/
@Override
public int getCurrentTtyMode(String callingPackage) {
- if (!canReadPhoneState(callingPackage, "getCurrentTtyMode")) {
- return TelecomManager.TTY_MODE_OFF;
- }
+ try {
+ Log.startSession("TSI.gCTM");
+ if (!canReadPhoneState(callingPackage, "getCurrentTtyMode")) {
+ return TelecomManager.TTY_MODE_OFF;
+ }
- synchronized (mLock) {
- return mCallsManager.getCurrentTtyMode();
+ synchronized (mLock) {
+ return mCallsManager.getCurrentTtyMode();
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -761,39 +946,48 @@
*/
@Override
public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
- synchronized (mLock) {
- Log.i(this, "Adding new incoming call with phoneAccountHandle %s",
- phoneAccountHandle);
- if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) {
- // TODO(sail): Add unit tests for adding incoming calls from a SIM call manager.
- if (isCallerSimCallManager() && TelephonyUtil.isPstnComponentName(
- phoneAccountHandle.getComponentName())) {
- Log.v(this, "Allowing call manager to add incoming call with PSTN handle");
- } else {
- mAppOpsManager.checkPackage(
- Binder.getCallingUid(),
- phoneAccountHandle.getComponentName().getPackageName());
- // Make sure it doesn't cross the UserHandle boundary
- enforceUserHandleMatchesCaller(phoneAccountHandle);
- }
-
- long token = Binder.clearCallingIdentity();
- try {
- Intent intent = new Intent(TelecomManager.ACTION_INCOMING_CALL);
- intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ try {
+ Log.startSession("TSI.aNIC");
+ synchronized (mLock) {
+ Log.i(this, "Adding new incoming call with phoneAccountHandle %s",
phoneAccountHandle);
- intent.putExtra(CallIntentProcessor.KEY_IS_INCOMING_CALL, true);
- if (extras != null) {
- intent.putExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
+ if (phoneAccountHandle != null &&
+ phoneAccountHandle.getComponentName() != null) {
+ // TODO(sail): Add unit tests for adding incoming calls from a SIM call
+ // manager.
+ if (isCallerSimCallManager() && TelephonyUtil.isPstnComponentName(
+ phoneAccountHandle.getComponentName())) {
+ Log.v(this, "Allowing call manager to add incoming call with PSTN" +
+ " handle");
+ } else {
+ mAppOpsManager.checkPackage(
+ Binder.getCallingUid(),
+ phoneAccountHandle.getComponentName().getPackageName());
+ // Make sure it doesn't cross the UserHandle boundary
+ enforceUserHandleMatchesCaller(phoneAccountHandle);
}
- CallIntentProcessor.processIncomingCallIntent(mCallsManager, intent);
- } finally {
- Binder.restoreCallingIdentity(token);
+
+ long token = Binder.clearCallingIdentity();
+ try {
+ Intent intent = new Intent(TelecomManager.ACTION_INCOMING_CALL);
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ phoneAccountHandle);
+ intent.putExtra(CallIntentProcessor.KEY_IS_INCOMING_CALL, true);
+ if (extras != null) {
+ intent.putExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
+ }
+ mCallIntentProcessorAdapter.processIncomingCallIntent(
+ mCallsManager, intent);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } else {
+ Log.w(this, "Null phoneAccountHandle. Ignoring request to add new" +
+ " incoming call");
}
- } else {
- Log.w(this,
- "Null phoneAccountHandle. Ignoring request to add new incoming call");
}
+ } finally {
+ Log.endSession();
}
}
@@ -802,31 +996,37 @@
*/
@Override
public void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
- synchronized (mLock) {
- if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) {
- mAppOpsManager.checkPackage(
- Binder.getCallingUid(),
- phoneAccountHandle.getComponentName().getPackageName());
+ try {
+ Log.startSession("TSI.aNUC");
+ synchronized (mLock) {
+ if (phoneAccountHandle != null &&
+ phoneAccountHandle.getComponentName() != null) {
+ mAppOpsManager.checkPackage(
+ Binder.getCallingUid(),
+ phoneAccountHandle.getComponentName().getPackageName());
- // Make sure it doesn't cross the UserHandle boundary
- enforceUserHandleMatchesCaller(phoneAccountHandle);
- long token = Binder.clearCallingIdentity();
+ // Make sure it doesn't cross the UserHandle boundary
+ enforceUserHandleMatchesCaller(phoneAccountHandle);
+ long token = Binder.clearCallingIdentity();
- try {
- Intent intent = new Intent(TelecomManager.ACTION_NEW_UNKNOWN_CALL);
- intent.putExtras(extras);
- intent.putExtra(CallIntentProcessor.KEY_IS_UNKNOWN_CALL, true);
- intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
- phoneAccountHandle);
- CallIntentProcessor.processUnknownCallIntent(mCallsManager, intent);
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Intent intent = new Intent(TelecomManager.ACTION_NEW_UNKNOWN_CALL);
+ intent.putExtras(extras);
+ intent.putExtra(CallIntentProcessor.KEY_IS_UNKNOWN_CALL, true);
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ phoneAccountHandle);
+ mCallIntentProcessorAdapter.processUnknownCallIntent(mCallsManager, intent);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } else {
+ Log.i(this,
+ "Null phoneAccountHandle or not initiated by Telephony. " +
+ "Ignoring request to add new unknown call.");
}
- } else {
- Log.i(this,
- "Null phoneAccountHandle or not initiated by Telephony. " +
- "Ignoring request to add new unknown call.");
}
+ } finally {
+ Log.endSession();
}
}
@@ -835,36 +1035,42 @@
*/
@Override
public void placeCall(Uri handle, Bundle extras, String callingPackage) {
- enforceCallingPackage(callingPackage);
- if (!canCallPhone(callingPackage, "placeCall")) {
- throw new SecurityException("Package " + callingPackage
- + " is not allowed to place phone calls");
- }
-
- // Note: we can still get here for the default/system dialer, even if the Phone
- // permission is turned off. This is because the default/system dialer is always
- // allowed to attempt to place a call (regardless of permission state), in case
- // it turns out to be an emergency call. If the permission is denied and the
- // call is being made to a non-emergency number, the call will be denied later on
- // by {@link UserCallIntentProcessor}.
-
- final boolean hasCallAppOp = mAppOpsManager.noteOp(AppOpsManager.OP_CALL_PHONE,
- Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED;
-
- final boolean hasCallPermission = mContext.checkCallingPermission(CALL_PHONE) ==
- PackageManager.PERMISSION_GRANTED;
-
- synchronized (mLock) {
- final UserHandle userHandle = Binder.getCallingUserHandle();
- long token = Binder.clearCallingIdentity();
- try {
- final Intent intent = new Intent(Intent.ACTION_CALL, handle);
- intent.putExtras(extras);
- new UserCallIntentProcessor(mContext, userHandle).processIntent(intent,
- callingPackage, hasCallAppOp && hasCallPermission);
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Log.startSession("TSI.pC");
+ enforceCallingPackage(callingPackage);
+ if (!canCallPhone(callingPackage, "placeCall")) {
+ throw new SecurityException("Package " + callingPackage
+ + " is not allowed to place phone calls");
}
+
+ // Note: we can still get here for the default/system dialer, even if the Phone
+ // permission is turned off. This is because the default/system dialer is always
+ // allowed to attempt to place a call (regardless of permission state), in case
+ // it turns out to be an emergency call. If the permission is denied and the
+ // call is being made to a non-emergency number, the call will be denied later on
+ // by {@link UserCallIntentProcessor}.
+
+ final boolean hasCallAppOp = mAppOpsManager.noteOp(AppOpsManager.OP_CALL_PHONE,
+ Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED;
+
+ final boolean hasCallPermission = mContext.checkCallingPermission(CALL_PHONE) ==
+ PackageManager.PERMISSION_GRANTED;
+
+ synchronized (mLock) {
+ final UserHandle userHandle = Binder.getCallingUserHandle();
+ long token = Binder.clearCallingIdentity();
+ try {
+ final Intent intent = new Intent(Intent.ACTION_CALL, handle);
+ intent.putExtras(extras);
+ mUserCallIntentProcessorFactory.create(mContext, userHandle)
+ .processIntent(
+ intent, callingPackage, hasCallAppOp && hasCallPermission);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ } finally {
+ Log.endSession();
}
}
@@ -873,39 +1079,61 @@
*/
@Override
public boolean enablePhoneAccount(PhoneAccountHandle accountHandle, boolean isEnabled) {
- enforceModifyPermission();
- synchronized (mLock) {
- long token = Binder.clearCallingIdentity();
- try {
- // enable/disable phone account
- return mPhoneAccountRegistrar.enablePhoneAccount(accountHandle, isEnabled);
- } finally {
- Binder.restoreCallingIdentity(token);
+ try {
+ Log.startSession("TSI.ePA");
+ enforceModifyPermission();
+ synchronized (mLock) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ // enable/disable phone account
+ return mPhoneAccountRegistrar.enablePhoneAccount(accountHandle, isEnabled);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
+ } finally {
+ Log.endSession();
}
}
@Override
public boolean setDefaultDialer(String packageName) {
- enforcePermission(MODIFY_PHONE_STATE);
- enforcePermission(WRITE_SECURE_SETTINGS);
- synchronized (mLock) {
- long token = Binder.clearCallingIdentity();
- try {
- final boolean result =
- DefaultDialerManager.setDefaultDialerApplication(mContext, packageName);
- if (result) {
- final Intent intent =
- new Intent(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED);
- intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
- packageName);
- mContext.sendBroadcastAsUser(intent,
- new UserHandle(ActivityManager.getCurrentUser()));
+ try {
+ Log.startSession("TSI.sDD");
+ enforcePermission(MODIFY_PHONE_STATE);
+ enforcePermission(WRITE_SECURE_SETTINGS);
+ synchronized (mLock) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ final boolean result =
+ mDefaultDialerManagerAdapter
+ .setDefaultDialerApplication(mContext, packageName);
+ if (result) {
+ final Intent intent =
+ new Intent(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED);
+ intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
+ packageName);
+ mContext.sendBroadcastAsUser(intent,
+ new UserHandle(ActivityManager.getCurrentUser()));
+ }
+ return result;
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- return result;
- } finally {
- Binder.restoreCallingIdentity(token);
}
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public List<ParcelableCallAnalytics> dumpCallAnalytics() {
+ try {
+ Log.startSession("TSI.dCA");
+ enforcePermission(DUMP);
+ return Arrays.asList(Analytics.dumpToParcelableAnalytics());
+ } finally {
+ Log.endSession();
}
}
@@ -938,10 +1166,21 @@
pw.increaseIndent();
mPhoneAccountRegistrar.dump(pw);
pw.decreaseIndent();
+
+ pw.println("Analytics:");
+ pw.increaseIndent();
+ Analytics.dump(pw);
+ pw.decreaseIndent();
}
Log.dumpCallEvents(pw);
}
+
+ // TODO: Add javadoc which points to TelecomManager
+ @Override
+ public void launchManageBlockedNumbersActivity(String callingPackageName) {
+ // TODO: Add implementation
+ }
};
private Context mContext;
@@ -950,12 +1189,20 @@
private PackageManager mPackageManager;
private CallsManager mCallsManager;
private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+ private final CallIntentProcessor.Adapter mCallIntentProcessorAdapter;
+ private final UserCallIntentProcessorFactory mUserCallIntentProcessorFactory;
+ private final DefaultDialerManagerAdapter mDefaultDialerManagerAdapter;
+ private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
private final TelecomSystem.SyncRoot mLock;
public TelecomServiceImpl(
Context context,
CallsManager callsManager,
PhoneAccountRegistrar phoneAccountRegistrar,
+ CallIntentProcessor.Adapter callIntentProcessorAdapter,
+ UserCallIntentProcessorFactory userCallIntentProcessorFactory,
+ DefaultDialerManagerAdapter defaultDialerManagerAdapter,
+ SubscriptionManagerAdapter subscriptionManagerAdapter,
TelecomSystem.SyncRoot lock) {
mContext = context;
mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
@@ -966,6 +1213,10 @@
mCallsManager = callsManager;
mLock = lock;
mPhoneAccountRegistrar = phoneAccountRegistrar;
+ mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
+ mDefaultDialerManagerAdapter = defaultDialerManagerAdapter;
+ mCallIntentProcessorAdapter = callIntentProcessorAdapter;
+ mSubscriptionManagerAdapter = subscriptionManagerAdapter;
}
public ITelecomService.Stub getBinder() {
@@ -976,63 +1227,9 @@
// Supporting methods for the ITelecomService interface implementation.
//
- private boolean isVisibleToCaller(PhoneAccountHandle accountHandle) {
- if (accountHandle == null) {
- return false;
- }
- return isVisibleToCaller(mPhoneAccountRegistrar.getPhoneAccount(accountHandle));
- }
-
- private boolean isVisibleToCaller(PhoneAccount account) {
- if (account == null) {
- return false;
- }
-
- // If this PhoneAccount has CAPABILITY_MULTI_USER, it should be visible to all users and
- // all profiles. Only Telephony and SIP accounts should have this capability.
- if (account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
- return true;
- }
-
- UserHandle phoneAccountUserHandle = account.getAccountHandle().getUserHandle();
- if (phoneAccountUserHandle == null) {
- return false;
- }
-
- if (phoneAccountUserHandle.equals(Binder.getCallingUserHandle())) {
- return true;
- }
-
- List<UserHandle> profileUserHandles;
- if (UserHandle.getCallingUserId() == UserHandle.USER_OWNER) {
- profileUserHandles = mUserManager.getUserProfiles();
- } else {
- // Otherwise, it has to be owned by the current caller's profile.
- profileUserHandles = new ArrayList<>(1);
- profileUserHandles.add(Binder.getCallingUserHandle());
- }
-
- return profileUserHandles.contains(phoneAccountUserHandle);
- }
-
- /**
- * Given a list of {@link PhoneAccountHandle}s, filter them to the ones that the calling
- * user can see.
- *
- * @param phoneAccountHandles Unfiltered list of account handles.
- *
- * @return {@link PhoneAccountHandle}s visible to the calling user and its profiles.
- */
- private List<PhoneAccountHandle> filterForAccountsVisibleToCaller(
- List<PhoneAccountHandle> phoneAccountHandles) {
- List<PhoneAccountHandle> profilePhoneAccountHandles =
- new ArrayList<>(phoneAccountHandles.size());
- for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandles) {
- if (isVisibleToCaller(phoneAccountHandle)) {
- profilePhoneAccountHandles.add(phoneAccountHandle);
- }
- }
- return profilePhoneAccountHandles;
+ private boolean isPhoneAccountHandleVisibleToCallingUser(
+ PhoneAccountHandle phoneAccountUserHandle, UserHandle callingUser) {
+ return mPhoneAccountRegistrar.getPhoneAccount(phoneAccountUserHandle, callingUser) != null;
}
private boolean isCallerSystemApp() {
@@ -1154,6 +1351,14 @@
}
}
+ private void enforceCrossUserPermission(int callingUid) {
+ if (callingUid != Process.SYSTEM_UID && callingUid != 0) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, "Must be system or have"
+ + " INTERACT_ACROSS_USERS_FULL permission");
+ }
+ }
+
private void enforceFeature(String feature) {
PackageManager pm = mContext.getPackageManager();
if (!pm.hasSystemFeature(feature)) {
@@ -1200,10 +1405,10 @@
}
private boolean isCallerSimCallManager() {
- PhoneAccountHandle accountHandle = null;
long token = Binder.clearCallingIdentity();
+ PhoneAccountHandle accountHandle = null;
try {
- accountHandle = mPhoneAccountRegistrar.getSimCallManager();
+ accountHandle = mPhoneAccountRegistrar.getSimCallManagerOfCurrentUser();
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1221,11 +1426,11 @@
private boolean isPrivilegedDialerCalling(String callingPackage) {
mAppOpsManager.checkPackage(Binder.getCallingUid(), callingPackage);
- return DefaultDialerManager.isDefaultOrSystemDialer(mContext, callingPackage);
+ return mDefaultDialerManagerAdapter.isDefaultOrSystemDialer(mContext, callingPackage);
}
private TelephonyManager getTelephonyManager() {
- return (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
}
/**
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index c3ab0dc..f484d6e 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -17,13 +17,23 @@
package com.android.server.telecom;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
+import com.android.server.telecom.BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory;
+import com.android.server.telecom.CallAudioManager.AudioServiceFactory;
+import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.net.Uri;
import android.os.UserHandle;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
/**
* Top-level Application class for Telecom.
*/
@@ -54,6 +64,28 @@
private static final IntentFilter USER_SWITCHED_FILTER =
new IntentFilter(Intent.ACTION_USER_SWITCHED);
+ private static final IntentFilter USER_STARTING_FILTER =
+ new IntentFilter(Intent.ACTION_USER_STARTING);
+
+ /** Intent filter for dialer secret codes. */
+ private static final IntentFilter DIALER_SECRET_CODE_FILTER;
+
+ /**
+ * Initializes the dialer secret code intent filter. Setup to handle the various secret codes
+ * which can be dialed (e.g. in format *#*#code#*#*) to trigger various behavior in Telecom.
+ */
+ static {
+ DIALER_SECRET_CODE_FILTER = new IntentFilter(
+ "android.provider.Telephony.SECRET_CODE");
+ DIALER_SECRET_CODE_FILTER.addDataScheme("android_secret_code");
+ DIALER_SECRET_CODE_FILTER
+ .addDataAuthority(DialerCodeReceiver.TELECOM_SECRET_CODE_DEBUG_ON, null);
+ DIALER_SECRET_CODE_FILTER
+ .addDataAuthority(DialerCodeReceiver.TELECOM_SECRET_CODE_DEBUG_OFF, null);
+ DIALER_SECRET_CODE_FILTER
+ .addDataAuthority(DialerCodeReceiver.TELECOM_SECRET_CODE_MARK, null);
+ }
+
private static TelecomSystem INSTANCE = null;
private final SyncRoot mLock = new SyncRoot() { };
@@ -67,13 +99,34 @@
private final TelecomBroadcastIntentProcessor mTelecomBroadcastIntentProcessor;
private final TelecomServiceImpl mTelecomServiceImpl;
private final ContactsAsyncHelper mContactsAsyncHelper;
+ private final DialerCodeReceiver mDialerCodeReceiver;
private final BroadcastReceiver mUserSwitchedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- int userHandleId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
- UserHandle currentUserHandle = new UserHandle(userHandleId);
- mPhoneAccountRegistrar.setCurrentUserHandle(currentUserHandle);
+ Log.startSession("TSSwR.oR");
+ try {
+ int userHandleId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
+ UserHandle currentUserHandle = new UserHandle(userHandleId);
+ mPhoneAccountRegistrar.setCurrentUserHandle(currentUserHandle);
+ mCallsManager.onUserSwitch(currentUserHandle);
+ } finally {
+ Log.endSession();
+ }
+ }
+ };
+
+ private final BroadcastReceiver mUserStartingReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.startSession("TSStR.oR");
+ try {
+ int userHandleId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
+ UserHandle addingUserHandle = new UserHandle(userHandleId);
+ mCallsManager.onUserStarting(addingUserHandle);
+ } finally {
+ Log.endSession();
+ }
}
};
@@ -91,16 +144,33 @@
public TelecomSystem(
Context context,
- MissedCallNotifier missedCallNotifier,
+ MissedCallNotifierImplFactory missedCallNotifierImplFactory,
CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
HeadsetMediaButtonFactory headsetMediaButtonFactory,
ProximitySensorManagerFactory proximitySensorManagerFactory,
- InCallWakeLockControllerFactory inCallWakeLockControllerFactory) {
+ InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
+ AudioServiceFactory audioServiceFactory,
+ BluetoothPhoneServiceImplFactory
+ bluetoothPhoneServiceImplFactory) {
mContext = context.getApplicationContext();
+ Log.setContext(mContext);
+ Log.initMd5Sum();
- mMissedCallNotifier = missedCallNotifier;
mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext);
- mContactsAsyncHelper = new ContactsAsyncHelper(mLock);
+ mContactsAsyncHelper = new ContactsAsyncHelper(
+ new ContactsAsyncHelper.ContentResolverAdapter() {
+ @Override
+ public InputStream openInputStream(Context context, Uri uri)
+ throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(uri);
+ }
+ });
+ BluetoothManager bluetoothManager = new BluetoothManager(mContext);
+ WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
+ SystemStateProvider systemStateProvider = new SystemStateProvider(mContext);
+
+ mMissedCallNotifier = missedCallNotifierImplFactory
+ .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar);
mCallsManager = new CallsManager(
mContext,
@@ -111,19 +181,41 @@
mPhoneAccountRegistrar,
headsetMediaButtonFactory,
proximitySensorManagerFactory,
- inCallWakeLockControllerFactory);
+ inCallWakeLockControllerFactory,
+ audioServiceFactory,
+ bluetoothManager,
+ wiredHeadsetManager,
+ systemStateProvider);
mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager, mLock);
mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
mContext.registerReceiver(mUserSwitchedReceiver, USER_SWITCHED_FILTER);
- mBluetoothPhoneServiceImpl = new BluetoothPhoneServiceImpl(
+ mContext.registerReceiver(mUserStartingReceiver, USER_STARTING_FILTER);
+
+ mBluetoothPhoneServiceImpl = bluetoothPhoneServiceImplFactory.makeBluetoothPhoneServiceImpl(
mContext, mLock, mCallsManager, mPhoneAccountRegistrar);
mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager);
mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
mContext, mCallsManager);
+
+ // Register the receiver for the dialer secret codes, used to enable extended logging.
+ mDialerCodeReceiver = new DialerCodeReceiver(mCallsManager);
+ mContext.registerReceiver(mDialerCodeReceiver, DIALER_SECRET_CODE_FILTER,
+ Manifest.permission.CONTROL_INCALL_EXPERIENCE, null);
+
mTelecomServiceImpl = new TelecomServiceImpl(
- mContext, mCallsManager, mPhoneAccountRegistrar, mLock);
+ mContext, mCallsManager, mPhoneAccountRegistrar,
+ new CallIntentProcessor.AdapterImpl(),
+ new UserCallIntentProcessorFactory() {
+ @Override
+ public UserCallIntentProcessor create(Context context, UserHandle userHandle) {
+ return new UserCallIntentProcessor(context, userHandle);
+ }
+ },
+ new TelecomServiceImpl.DefaultDialerManagerAdapterImpl(),
+ new TelecomServiceImpl.SubscriptionManagerAdapterImpl(),
+ mLock);
}
@VisibleForTesting
diff --git a/src/com/android/server/telecom/TelecomWakeLock.java b/src/com/android/server/telecom/TelecomWakeLock.java
new file mode 100644
index 0000000..9fca19e
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomWakeLock.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.PowerManager;
+
+/**
+ * Container for PowerManager / PowerManager.WakeLock access in telecom to facilitate unit testing.
+ */
+public class TelecomWakeLock {
+
+ public class WakeLockAdapter {
+
+ private PowerManager.WakeLock mWakeLock;
+
+ public WakeLockAdapter(PowerManager.WakeLock wakeLock) {
+ mWakeLock = wakeLock;
+ }
+
+ public void acquire() {
+ mWakeLock.acquire();
+ }
+
+ public boolean isHeld() {
+ return mWakeLock.isHeld();
+ }
+
+ public void release(int flags) {
+ mWakeLock.release(flags);
+ }
+
+ public void setReferenceCounted(boolean isReferencedCounted){
+ mWakeLock.setReferenceCounted(isReferencedCounted);
+ }
+
+ }
+
+ private static final String TAG = "TelecomWakeLock";
+
+ private Context mContext;
+ private int mWakeLockLevel;
+ private String mWakeLockTag;
+ private WakeLockAdapter mWakeLock;
+
+ public TelecomWakeLock(Context context, int wakeLockLevel, String wakeLockTag) {
+ mContext = context;
+ mWakeLockLevel = wakeLockLevel;
+ mWakeLockTag = wakeLockTag;
+ mWakeLock = getWakeLockFromPowerManager();
+ }
+
+ // Used For Testing
+ public TelecomWakeLock(Context context, WakeLockAdapter wakeLockAdapter, int wakeLockLevel,
+ String wakeLockTag) {
+ mContext = context;
+ mWakeLockLevel = wakeLockLevel;
+ mWakeLockTag = wakeLockTag;
+ mWakeLock = wakeLockAdapter;
+ }
+
+ private WakeLockAdapter getWakeLockFromPowerManager() {
+ PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ WakeLockAdapter adapter = null;
+ if(powerManager.isWakeLockLevelSupported(mWakeLockLevel)) {
+ PowerManager.WakeLock wakeLock = powerManager.newWakeLock(mWakeLockLevel, mWakeLockTag);
+ adapter = new WakeLockAdapter(wakeLock);
+ }
+ return adapter;
+ }
+
+ public boolean isHeld() {
+ return mWakeLock != null && mWakeLock.isHeld();
+ }
+
+ public void acquire() {
+ if(mWakeLock == null) {
+ Log.i(TAG, "Can not acquire WakeLock (not supported) with level: " + mWakeLockLevel);
+ return;
+ }
+
+ if (!isHeld()) {
+ mWakeLock.acquire();
+ Log.i(TAG, "Acquiring WakeLock with id: " + mWakeLockLevel);
+ } else {
+ Log.i(TAG, "WakeLock already acquired for id: " + mWakeLockLevel);
+ }
+ }
+
+ public void release(int flags) {
+ if (mWakeLock == null) {
+ Log.i(TAG, "Can not release WakeLock (not supported) with id: " + mWakeLockLevel);
+ return;
+ }
+
+ if (isHeld()) {
+ mWakeLock.release(flags);
+ Log.i(TAG, "Releasing WakeLock with id: " + mWakeLockLevel);
+ } else {
+ Log.i(TAG, "WakeLock already released with id: " + mWakeLockLevel);
+ }
+ }
+
+ public void setReferenceCounted(boolean isReferencedCounted) {
+ if (mWakeLock == null) {
+ return;
+ }
+ mWakeLock.setReferenceCounted(isReferencedCounted);
+ }
+
+ @Override
+ public String toString() {
+ if(mWakeLock != null) {
+ return mWakeLock.toString();
+ } else {
+ return "null";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
index 5f43ee9..69adaf9 100644
--- a/src/com/android/server/telecom/TelephonyUtil.java
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -23,6 +23,8 @@
import android.telecom.PhoneAccountHandle;
import android.telephony.PhoneNumberUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* Utilities to deal with the system telephony services. The system telephony services are treated
* differently from 3rd party services in some situations (emergency calls, audio focus, etc...).
@@ -45,7 +47,8 @@
* account are not expected to be displayed in the UI, so the description, etc are not
* populated.
*/
- static PhoneAccount getDefaultEmergencyPhoneAccount() {
+ @VisibleForTesting
+ public static PhoneAccount getDefaultEmergencyPhoneAccount() {
return PhoneAccount.builder(DEFAULT_EMERGENCY_PHONE_ACCOUNT_HANDLE, "E")
.setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
PhoneAccount.CAPABILITY_CALL_PROVIDER |
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index f047971..9824f88 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -98,4 +98,37 @@
return get(contentResolver, "call_remove_unbind_in_call_services_delay",
2000L /* 2 seconds */);
}
+
+ /**
+ * Returns the amount of time for which bluetooth is considered connected after requesting
+ * connection. This compensates for the amount of time it takes for the audio route to
+ * actually change to bluetooth.
+ */
+ public static long getBluetoothPendingTimeoutMillis(ContentResolver contentResolver) {
+ return get(contentResolver, "bluetooth_pending_timeout_millis", 5000L);
+ }
+
+ /**
+ * Returns the amount of time after a Logging session has been started that Telecom is set to
+ * perform a sweep to check and make sure that the session is still not incomplete (stale).
+ */
+ public static long getStaleSessionCleanupTimeoutMillis(ContentResolver contentResolver) {
+ return get(contentResolver, "stale_session_cleanup_timeout_millis",
+ Log.DEFAULT_SESSION_TIMEOUT_MS);
+ }
+
+ /**
+ * Returns the amount of time to wait for the call screening service to allow or disallow a
+ * call.
+ */
+ public static long getCallScreeningTimeoutMillis(ContentResolver contentResolver) {
+ return get(contentResolver, "call_screening_timeout", 5000L /* 5 seconds */);
+ }
+
+ /**
+ * Returns the amount of time to wait for the block checker to allow or disallow a call.
+ */
+ public static long getBlockCheckTimeoutMillis(ContentResolver contentResolver) {
+ return get(contentResolver, "block_check_timeout_millis", 500L);
+ }
}
diff --git a/src/com/android/server/telecom/TtyManager.java b/src/com/android/server/telecom/TtyManager.java
index e0a9e9c..25284e4 100644
--- a/src/com/android/server/telecom/TtyManager.java
+++ b/src/com/android/server/telecom/TtyManager.java
@@ -112,15 +112,20 @@
private final class TtyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- Log.v(TtyManager.this, "onReceive, action: %s", action);
- if (action.equals(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED)) {
- int newPreferredTtyMode = intent.getIntExtra(
- TelecomManager.EXTRA_TTY_PREFERRED_MODE, TelecomManager.TTY_MODE_OFF);
- if (mPreferredTtyMode != newPreferredTtyMode) {
- mPreferredTtyMode = newPreferredTtyMode;
- updateCurrentTtyMode();
+ Log.startSession("TBR.oR");
+ try {
+ String action = intent.getAction();
+ Log.v(TtyManager.this, "onReceive, action: %s", action);
+ if (action.equals(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED)) {
+ int newPreferredTtyMode = intent.getIntExtra(
+ TelecomManager.EXTRA_TTY_PREFERRED_MODE, TelecomManager.TTY_MODE_OFF);
+ if (mPreferredTtyMode != newPreferredTtyMode) {
+ mPreferredTtyMode = newPreferredTtyMode;
+ updateCurrentTtyMode();
+ }
}
+ } finally {
+ Log.endSession();
}
}
}
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
new file mode 100644
index 0000000..a304401
--- /dev/null
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+public final class UserUtil {
+
+ private UserUtil() {
+ }
+
+ private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
+ UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ return userManager.getUserInfo(userHandle.getIdentifier());
+ }
+
+ public static boolean isManagedProfile(Context context, UserHandle userHandle) {
+ UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
+ return userInfo != null && userInfo.isManagedProfile();
+ }
+
+ public static boolean isProfile(Context context, UserHandle userHandle) {
+ UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
+ return userInfo != null && userInfo.profileGroupId != userInfo.id;
+ }
+}
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index b531242..9d17139 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -126,15 +126,20 @@
*/
@Override
public void receiveSessionModifyRequest(VideoProfile videoProfile) {
- synchronized (mLock) {
- logFromVideoProvider("receiveSessionModifyRequest: " + videoProfile);
+ try {
+ Log.startSession("VPP.rSMR");
+ synchronized (mLock) {
+ logFromVideoProvider("receiveSessionModifyRequest: " + videoProfile);
- // Inform other Telecom components of the session modification request.
- for (Listener listener : mListeners) {
- listener.onSessionModifyRequestReceived(mCall, videoProfile);
+ // Inform other Telecom components of the session modification request.
+ for (Listener listener : mListeners) {
+ listener.onSessionModifyRequestReceived(mCall, videoProfile);
+ }
+
+ VideoProviderProxy.this.receiveSessionModifyRequest(videoProfile);
}
-
- VideoProviderProxy.this.receiveSessionModifyRequest(videoProfile);
+ } finally {
+ Log.endSession();
}
}
diff --git a/src/com/android/server/telecom/WiredHeadsetManager.java b/src/com/android/server/telecom/WiredHeadsetManager.java
index ef5f38c..253dfca 100644
--- a/src/com/android/server/telecom/WiredHeadsetManager.java
+++ b/src/com/android/server/telecom/WiredHeadsetManager.java
@@ -22,6 +22,7 @@
import android.content.IntentFilter;
import android.media.AudioManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.Collections;
@@ -29,8 +30,10 @@
import java.util.concurrent.ConcurrentHashMap;
/** Listens for and caches headset state. */
-class WiredHeadsetManager {
- interface Listener {
+@VisibleForTesting
+public class WiredHeadsetManager {
+ @VisibleForTesting
+ public interface Listener {
void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
}
@@ -38,11 +41,16 @@
private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
- boolean isPluggedIn = mAudioManager.isWiredHeadsetOn();
- Log.v(WiredHeadsetManager.this, "ACTION_HEADSET_PLUG event, plugged in: %b",
- isPluggedIn);
- onHeadsetPluggedInChanged(isPluggedIn);
+ Log.startSession("WHBR.oR");
+ try {
+ if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ boolean isPluggedIn = mAudioManager.isWiredHeadsetOn();
+ Log.v(WiredHeadsetManager.this, "ACTION_HEADSET_PLUG event, plugged in: %b",
+ isPluggedIn);
+ onHeadsetPluggedInChanged(isPluggedIn);
+ }
+ } finally {
+ Log.endSession();
}
}
}
@@ -58,7 +66,7 @@
private final Set<Listener> mListeners = Collections.newSetFromMap(
new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
- WiredHeadsetManager(Context context) {
+ public WiredHeadsetManager(Context context) {
mReceiver = new WiredHeadsetBroadcastReceiver();
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -69,7 +77,8 @@
context.registerReceiver(mReceiver, intentFilter);
}
- void addListener(Listener listener) {
+ @VisibleForTesting
+ public void addListener(Listener listener) {
mListeners.add(listener);
}
@@ -79,7 +88,8 @@
}
}
- boolean isPluggedIn() {
+ @VisibleForTesting
+ public boolean isPluggedIn() {
return mIsPluggedIn;
}
diff --git a/src/com/android/server/telecom/components/PrimaryCallReceiver.java b/src/com/android/server/telecom/components/PrimaryCallReceiver.java
index 5198862..a05f04e 100644
--- a/src/com/android/server/telecom/components/PrimaryCallReceiver.java
+++ b/src/com/android/server/telecom/components/PrimaryCallReceiver.java
@@ -1,5 +1,6 @@
package com.android.server.telecom.components;
+import com.android.server.telecom.Log;
import com.android.server.telecom.TelecomSystem;
import android.content.BroadcastReceiver;
@@ -16,9 +17,11 @@
@Override
public void onReceive(Context context, Intent intent) {
+ Log.startSession("PCR.oR");
synchronized (getTelecomSystem().getLock()) {
getTelecomSystem().getCallIntentProcessor().processIntent(intent);
}
+ Log.endSession();
}
@Override
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 7856094..8e4ae57 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -20,19 +20,26 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
+import android.media.IAudioService;
import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.ServiceManager;
import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.server.telecom.BluetoothPhoneServiceImpl;
import com.android.server.telecom.CallerInfoAsyncQueryFactory;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.HeadsetMediaButton;
import com.android.server.telecom.HeadsetMediaButtonFactory;
import com.android.server.telecom.InCallWakeLockControllerFactory;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.ProximitySensorManagerFactory;
import com.android.server.telecom.InCallWakeLockController;
import com.android.server.telecom.Log;
import com.android.server.telecom.ProximitySensorManager;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.TelecomWakeLock;
import com.android.server.telecom.ui.MissedCallNotifierImpl;
/**
@@ -64,7 +71,15 @@
TelecomSystem.setInstance(
new TelecomSystem(
context,
- new MissedCallNotifierImpl(context.getApplicationContext()),
+ new MissedCallNotifierImpl.MissedCallNotifierImplFactory() {
+ @Override
+ public MissedCallNotifierImpl makeMissedCallNotifierImpl(
+ Context context,
+ PhoneAccountRegistrar phoneAccountRegistrar) {
+ return new MissedCallNotifierImpl(context,
+ phoneAccountRegistrar);
+ }
+ },
new CallerInfoAsyncQueryFactory() {
@Override
public CallerInfoAsyncQuery startQuery(int token, Context context,
@@ -92,16 +107,43 @@
public ProximitySensorManager create(
Context context,
CallsManager callsManager) {
- return new ProximitySensorManager(context, callsManager);
+ return new ProximitySensorManager(
+ new TelecomWakeLock(
+ context,
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
+ ProximitySensorManager.class.getSimpleName()),
+ callsManager);
}
},
new InCallWakeLockControllerFactory() {
@Override
public InCallWakeLockController create(Context context,
CallsManager callsManager) {
- return new InCallWakeLockController(context, callsManager);
+ return new InCallWakeLockController(
+ new TelecomWakeLock(context,
+ PowerManager.FULL_WAKE_LOCK,
+ InCallWakeLockController.class.getSimpleName()),
+ callsManager);
}
- }));
+ },
+ new CallAudioManager.AudioServiceFactory() {
+ @Override
+ public IAudioService getAudioService() {
+ return IAudioService.Stub.asInterface(
+ ServiceManager.getService(Context.AUDIO_SERVICE));
+ }
+ },
+ new BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory() {
+ @Override
+ public BluetoothPhoneServiceImpl makeBluetoothPhoneServiceImpl(
+ Context context, TelecomSystem.SyncRoot lock,
+ CallsManager callsManager,
+ PhoneAccountRegistrar phoneAccountRegistrar) {
+ return new BluetoothPhoneServiceImpl(context, lock,
+ callsManager, phoneAccountRegistrar);
+ }
+ }
+ ));
}
if (BluetoothAdapter.getDefaultAdapter() != null) {
context.startService(new Intent(context, BluetoothPhoneService.class));
diff --git a/src/com/android/server/telecom/components/UserCallActivity.java b/src/com/android/server/telecom/components/UserCallActivity.java
index d88e09e..39cb4f9 100644
--- a/src/com/android/server/telecom/components/UserCallActivity.java
+++ b/src/com/android/server/telecom/components/UserCallActivity.java
@@ -52,18 +52,23 @@
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
- // TODO: Figure out if there is something to restore from bundle.
- // See OutgoingCallBroadcaster in services/Telephony for more.
- Intent intent = getIntent();
- verifyCallAction(intent);
- final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
- final UserHandle userHandle = new UserHandle(userManager.getUserHandle());
- // Once control flow has passed to this activity, it is no longer guaranteed that we can
- // accurately determine whether the calling package has the CALL_PHONE runtime permission.
- // At this point in time we trust that the ActivityManager has already performed this
- // validation before starting this activity.
- new UserCallIntentProcessor(this, userHandle).processIntent(getIntent(),
- getCallingPackage(), true /* hasCallAppOp*/);
+ Log.startSession("UCA.oC");
+ try {
+ // TODO: Figure out if there is something to restore from bundle.
+ // See OutgoingCallBroadcaster in services/Telephony for more.
+ Intent intent = getIntent();
+ verifyCallAction(intent);
+ final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ final UserHandle userHandle = new UserHandle(userManager.getUserHandle());
+ // Once control flow has passed to this activity, it is no longer guaranteed that we can
+ // accurately determine whether the calling package has the CALL_PHONE runtime permission.
+ // At this point in time we trust that the ActivityManager has already performed this
+ // validation before starting this activity.
+ new UserCallIntentProcessor(this, userHandle).processIntent(getIntent(),
+ getCallingPackage(), true /* hasCallAppOp*/);
+ } finally {
+ Log.endSession();
+ }
finish();
}
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
index f6a0027..65258d9 100644
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -125,6 +125,10 @@
intent.putExtra(CallIntentProcessor.KEY_IS_PRIVILEGED_DIALER,
isDefaultOrSystemDialer(callingPackageName));
+
+ // Save the user handle of current user before forwarding the intent to primary user.
+ intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, mUserHandle);
+
sendBroadcastToReceiver(intent);
}
@@ -162,7 +166,7 @@
intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.setClass(mContext, PrimaryCallReceiver.class);
Log.d(this, "Sending broadcast as user to CallReceiver");
- mContext.sendBroadcastAsUser(intent, UserHandle.OWNER);
+ mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
return true;
}
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessorFactory.java b/src/com/android/server/telecom/components/UserCallIntentProcessorFactory.java
new file mode 100644
index 0000000..d37cd06
--- /dev/null
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessorFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.components;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+public interface UserCallIntentProcessorFactory {
+ UserCallIntentProcessor create(Context context, UserHandle userHandle);
+}
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index a864a83..67ca641 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -16,8 +16,14 @@
package com.android.server.telecom.ui;
+import static android.Manifest.permission.READ_PHONE_STATE;
+
import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallerInfoAsyncQueryFactory;
@@ -27,7 +33,9 @@
import com.android.server.telecom.ContactsAsyncHelper;
import com.android.server.telecom.Log;
import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.R;
+import com.android.server.telecom.Runnable;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
@@ -40,6 +48,7 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
@@ -49,6 +58,7 @@
import android.os.Binder;
import android.os.UserHandle;
import android.provider.CallLog.Calls;
+import android.telecom.DefaultDialerManager;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telephony.PhoneNumberUtils;
@@ -57,9 +67,15 @@
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
+import com.android.internal.telephony.CallerInfo;
+
import java.lang.Override;
import java.lang.String;
+import java.util.List;
import java.util.Locale;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
// TODO: Needed for move to system service: import com.android.internal.R;
@@ -75,6 +91,24 @@
*/
public class MissedCallNotifierImpl extends CallsManagerListenerBase implements MissedCallNotifier {
+ public interface MissedCallNotifierImplFactory {
+ MissedCallNotifier makeMissedCallNotifierImpl(Context context,
+ PhoneAccountRegistrar phoneAccountRegistrar);
+ }
+
+ public interface NotificationBuilderFactory {
+ Notification.Builder getBuilder(Context context);
+ }
+
+ private static class DefaultNotificationBuilderFactory implements NotificationBuilderFactory {
+ public DefaultNotificationBuilderFactory() {}
+
+ @Override
+ public Notification.Builder getBuilder(Context context) {
+ return new Notification.Builder(context);
+ }
+ }
+
private static final String[] CALL_LOG_PROJECTION = new String[] {
Calls._ID,
Calls.NUMBER,
@@ -94,21 +128,32 @@
private static final int MISSED_CALL_NOTIFICATION_ID = 1;
private final Context mContext;
+ private final PhoneAccountRegistrar mPhoneAccountRegistrar;
private final NotificationManager mNotificationManager;
-
+ private final NotificationBuilderFactory mNotificationBuilderFactory;
private final ComponentName mNotificationComponent;
+ private UserHandle mCurrentUserHandle;
// Used to track the number of missed calls.
- private int mMissedCallCount = 0;
+ private ConcurrentMap<UserHandle, AtomicInteger> mMissedCallCounts;
- public MissedCallNotifierImpl(Context context) {
+ public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar) {
+ this(context, phoneAccountRegistrar, new DefaultNotificationBuilderFactory());
+ }
+
+ public MissedCallNotifierImpl(Context context,
+ PhoneAccountRegistrar phoneAccountRegistrar,
+ NotificationBuilderFactory notificationBuilderFactory) {
mContext = context;
+ mPhoneAccountRegistrar = phoneAccountRegistrar;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
final String notificationComponent = context.getString(R.string.notification_component);
+ mNotificationBuilderFactory = notificationBuilderFactory;
mNotificationComponent = notificationComponent != null
? ComponentName.unflattenFromString(notificationComponent) : null;
+ mMissedCallCounts = new ConcurrentHashMap<>();
}
/** {@inheritDoc} */
@@ -122,10 +167,19 @@
/** Clears missed call notification and marks the call log's missed calls as read. */
@Override
- public void clearMissedCalls() {
- AsyncTask.execute(new Runnable() {
+ public void clearMissedCalls(UserHandle userHandle) {
+ // If the default dialer is showing the missed call notification then it will modify the
+ // call log and we don't have to do anything here.
+ if (!shouldManageNotificationThroughDefaultDialer(userHandle)) {
+ markMissedCallsAsRead(userHandle);
+ }
+ cancelMissedCallNotification(userHandle);
+ }
+
+ private void markMissedCallsAsRead(final UserHandle userHandle) {
+ AsyncTask.execute(new Runnable("MCNI.mMCAR") {
@Override
- public void run() {
+ public void loggedRun() {
// Clear the list of new missed calls from the call log.
ContentValues values = new ContentValues();
values.put(Calls.NEW, 0);
@@ -136,26 +190,27 @@
where.append(Calls.TYPE);
where.append(" = ?");
try {
- mContext.getContentResolver().update(Calls.CONTENT_URI, values,
+ Uri callsUri = ContentProvider
+ .maybeAddUserId(Calls.CONTENT_URI, userHandle.getIdentifier());
+ mContext.getContentResolver().update(callsUri, values,
where.toString(), new String[]{ Integer.toString(Calls.
MISSED_TYPE) });
} catch (IllegalArgumentException e) {
Log.w(this, "ContactsProvider update command failed", e);
}
}
- });
- cancelMissedCallNotification();
+ }.prepare());
}
/**
* Broadcasts missed call notification to custom component if set.
- * @param number The phone number associated with the notification. null if
- * no call.
- * @param count The number of calls associated with the notification.
+ * Currently the component is set in phone capable android wear device.
+ * @param userHandle The user that has the missed call(s).
* @return {@code true} if the broadcast was sent. {@code false} otherwise.
*/
- private boolean sendNotificationCustomComponent(Call call, int count) {
+ private boolean sendNotificationCustomComponent(Call call, UserHandle userHandle) {
if (mNotificationComponent != null) {
+ int count = mMissedCallCounts.get(userHandle).get();
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.setComponent(mNotificationComponent);
@@ -164,7 +219,7 @@
intent.putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER,
call != null ? call.getPhoneNumber() : null);
intent.putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT,
- createClearMissedCallsPendingIntent());
+ createClearMissedCallsPendingIntent(userHandle));
if (count == 1 && call != null) {
@@ -174,7 +229,7 @@
if (!TextUtils.isEmpty(handle) && !TextUtils.equals(handle,
mContext.getString(R.string.handle_restricted))) {
intent.putExtra(TelecomManager.EXTRA_CALL_BACK_INTENT,
- createCallBackPendingIntent(handleUri));
+ createCallBackPendingIntent(handleUri, userHandle));
}
}
@@ -186,15 +241,77 @@
}
/**
+ * Returns the missed-call notificatino intent to send to the default dialer for the given user. * Note, the passed in userHandle is always the non-managed user for SIM calls (multi-user
+ * calls). In this case we return the default dialer for the logged in user. This is never the
+ * managed (work profile) dialer.
+ *
+ * For non-multi-user calls (3rd party phone accounts), the passed in userHandle is the user
+ * handle of the phone account. This could be a managed user. In that case we return the default
+ * dialer for the given user which could be a managed (work profile) dialer.
+ */
+ private Intent getShowMissedCallIntentForDefaultDialer(UserHandle userHandle) {
+ String dialerPackage = DefaultDialerManager
+ .getDefaultDialerApplication(mContext, userHandle.getIdentifier());
+ if (TextUtils.isEmpty(dialerPackage)) {
+ return null;
+ }
+ return new Intent(TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION)
+ .setPackage(dialerPackage);
+ }
+
+ private boolean shouldManageNotificationThroughDefaultDialer(UserHandle userHandle) {
+ Intent intent = getShowMissedCallIntentForDefaultDialer(userHandle);
+ if (intent == null) {
+ return false;
+ }
+
+ List<ResolveInfo> receivers = mContext.getPackageManager()
+ .queryBroadcastReceiversAsUser(intent, 0, userHandle.getIdentifier());
+ return receivers.size() > 0;
+ }
+
+ private void sendNotificationThroughDefaultDialer(Call call, UserHandle userHandle) {
+ int count = mMissedCallCounts.get(userHandle).get();
+ Intent intent = getShowMissedCallIntentForDefaultDialer(userHandle)
+ .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, count)
+ .putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER,
+ call != null ? call.getPhoneNumber() : null);
+
+ Log.w(this, "Showing missed calls through default dialer.");
+ mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE);
+ }
+
+ /**
* Create a system notification for the missed call.
*
* @param call The missed call.
*/
@Override
public void showMissedCallNotification(Call call) {
- mMissedCallCount++;
+ final PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+ final PhoneAccount phoneAccount =
+ mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
+ UserHandle userHandle;
+ if (phoneAccount != null &&
+ phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
+ userHandle = mCurrentUserHandle;
+ } else {
+ userHandle = phoneAccountHandle.getUserHandle();
+ }
+ showMissedCallNotification(call, userHandle);
+ }
- if (sendNotificationCustomComponent(call, mMissedCallCount)) {
+ private void showMissedCallNotification(Call call, UserHandle userHandle) {
+ mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
+ int missCallCounts = mMissedCallCounts.get(userHandle).incrementAndGet();
+
+ if (sendNotificationCustomComponent(call, userHandle)) {
+ return;
+ }
+
+ if (shouldManageNotificationThroughDefaultDialer(userHandle)) {
+ sendNotificationThroughDefaultDialer(call, userHandle);
return;
}
@@ -204,18 +321,26 @@
// Display the first line of the notification:
// 1 missed call: <caller name || handle>
// More than 1 missed call: <number of calls> + "missed calls"
- if (mMissedCallCount == 1) {
- titleResId = R.string.notification_missedCallTitle;
+ if (missCallCounts == 1) {
expandedText = getNameForCall(call);
+
+ CallerInfo ci = call.getCallerInfo();
+ if (ci != null && ci.userType == CallerInfo.USER_TYPE_WORK) {
+ titleResId = R.string.notification_missedWorkCallTitle;
+ } else {
+ titleResId = R.string.notification_missedCallTitle;
+ }
} else {
titleResId = R.string.notification_missedCallsTitle;
expandedText =
- mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount);
+ mContext.getString(R.string.notification_missedCallsMsg, missCallCounts);
}
// Create a public viewable version of the notification, suitable for display when sensitive
// notification content is hidden.
- Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ // We use user's context here to make sure notification is badged if it is a managed user.
+ Context contextForUser = getContextForUser(userHandle);
+ Notification.Builder publicBuilder = mNotificationBuilderFactory.getBuilder(contextForUser);
publicBuilder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
.setColor(mContext.getResources().getColor(R.color.theme_color))
.setWhen(call.getCreationTimeMillis())
@@ -224,20 +349,20 @@
// Notification details shows that there are missed call(s), but does not reveal
// the missed caller information.
.setContentText(mContext.getText(titleResId))
- .setContentIntent(createCallLogPendingIntent())
+ .setContentIntent(createCallLogPendingIntent(userHandle))
.setAutoCancel(true)
- .setDeleteIntent(createClearMissedCallsPendingIntent());
+ .setDeleteIntent(createClearMissedCallsPendingIntent(userHandle));
// Create the notification suitable for display when sensitive information is showing.
- Notification.Builder builder = new Notification.Builder(mContext);
+ Notification.Builder builder = mNotificationBuilderFactory.getBuilder(contextForUser);
builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
.setColor(mContext.getResources().getColor(R.color.theme_color))
.setWhen(call.getCreationTimeMillis())
.setContentTitle(mContext.getText(titleResId))
.setContentText(expandedText)
- .setContentIntent(createCallLogPendingIntent())
+ .setContentIntent(createCallLogPendingIntent(userHandle))
.setAutoCancel(true)
- .setDeleteIntent(createClearMissedCallsPendingIntent())
+ .setDeleteIntent(createClearMissedCallsPendingIntent(userHandle))
// Include a public version of the notification to be shown when the missed call
// notification is shown on the user's lock screen and they have chosen to hide
// sensitive notification information.
@@ -247,19 +372,19 @@
String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
// Add additional actions when there is only 1 missed call, like call-back and SMS.
- if (mMissedCallCount == 1) {
+ if (missCallCounts == 1) {
Log.d(this, "Add actions with number %s.", Log.piiHandle(handle));
if (!TextUtils.isEmpty(handle)
&& !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) {
builder.addAction(R.drawable.ic_phone_24dp,
mContext.getString(R.string.notification_missedCall_call_back),
- createCallBackPendingIntent(handleUri));
+ createCallBackPendingIntent(handleUri, userHandle));
if (canRespondViaSms(call)) {
builder.addAction(R.drawable.ic_message_24dp,
mContext.getString(R.string.notification_missedCall_message),
- createSendSmsFromNotificationPendingIntent(handleUri));
+ createSendSmsFromNotificationPendingIntent(handleUri, userHandle));
}
}
@@ -274,7 +399,7 @@
}
} else {
Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle),
- mMissedCallCount);
+ missCallCounts);
}
Notification notification = builder.build();
@@ -284,26 +409,31 @@
long token = Binder.clearCallingIdentity();
try {
mNotificationManager.notifyAsUser(
- null /* tag */, MISSED_CALL_NOTIFICATION_ID, notification, UserHandle.CURRENT);
+ null /* tag */, MISSED_CALL_NOTIFICATION_ID, notification, userHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
}
+
/** Cancels the "missed call" notification. */
- private void cancelMissedCallNotification() {
+ private void cancelMissedCallNotification(UserHandle userHandle) {
// Reset the number of missed calls to 0.
- mMissedCallCount = 0;
+ mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
+ mMissedCallCounts.get(userHandle).set(0);
+ if (sendNotificationCustomComponent(null, userHandle)) {
+ return;
+ }
- if (sendNotificationCustomComponent(null, mMissedCallCount)) {
+ if (shouldManageNotificationThroughDefaultDialer(userHandle)) {
+ sendNotificationThroughDefaultDialer(null, userHandle);
return;
}
long token = Binder.clearCallingIdentity();
try {
- mNotificationManager.cancelAsUser(null, MISSED_CALL_NOTIFICATION_ID,
- UserHandle.CURRENT);
+ mNotificationManager.cancelAsUser(null, MISSED_CALL_NOTIFICATION_ID, userHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -367,22 +497,22 @@
*
* @return The pending intent.
*/
- private PendingIntent createCallLogPendingIntent() {
+ private PendingIntent createCallLogPendingIntent(UserHandle userHandle) {
Intent intent = new Intent(Intent.ACTION_VIEW, null);
intent.setType(Calls.CONTENT_TYPE);
TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
taskStackBuilder.addNextIntent(intent);
- return taskStackBuilder.getPendingIntent(0, 0, null, UserHandle.CURRENT);
+ return taskStackBuilder.getPendingIntent(0, 0, null, userHandle);
}
/**
* Creates an intent to be invoked when the missed call notification is cleared.
*/
- private PendingIntent createClearMissedCallsPendingIntent() {
+ private PendingIntent createClearMissedCallsPendingIntent(UserHandle userHandle) {
return createTelecomPendingIntent(
- TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null);
+ TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null, userHandle);
}
/**
@@ -391,19 +521,22 @@
*
* @param handle The handle to call back.
*/
- private PendingIntent createCallBackPendingIntent(Uri handle) {
+ private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) {
return createTelecomPendingIntent(
- TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle);
+ TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle,
+ userHandle);
}
/**
* Creates an intent to be invoked when the user opts to "send sms" from the missed call
* notification.
*/
- private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) {
+ private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle,
+ UserHandle userHandle) {
return createTelecomPendingIntent(
TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION,
- Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null));
+ Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null),
+ userHandle);
}
/**
@@ -413,8 +546,10 @@
* @param action The intent action.
* @param data The intent data.
*/
- private PendingIntent createTelecomPendingIntent(String action, Uri data) {
+ private PendingIntent createTelecomPendingIntent(String action, Uri data,
+ UserHandle userHandle) {
Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
+ intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle);
return PendingIntent.getBroadcast(mContext, 0, intent, 0);
}
@@ -436,12 +571,13 @@
* Adds the missed call notification on startup if there are unread missed calls.
*/
@Override
- public void updateOnStartup(
+ public void reloadFromDatabase(
final TelecomSystem.SyncRoot lock,
final CallsManager callsManager,
final ContactsAsyncHelper contactsAsyncHelper,
- final CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory) {
- Log.d(this, "updateOnStartup()...");
+ final CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
+ final UserHandle userHandle) {
+ Log.d(this, "reloadFromDatabase()...");
// instantiate query handler
AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
@@ -450,6 +586,7 @@
Log.d(MissedCallNotifierImpl.this, "onQueryComplete()...");
if (cursor != null) {
try {
+ mMissedCallCounts.remove(userHandle);
while (cursor.moveToNext()) {
// Get data about the missed call from the cursor
final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER);
@@ -470,9 +607,10 @@
synchronized (lock) {
// Convert the data to a call object
- Call call = new Call(mContext, callsManager, lock,
- null, contactsAsyncHelper, callerInfoAsyncQueryFactory,
- null, null, null, null, true, false);
+ Call call = new Call(Call.CALL_ID_UNKNOWN, mContext, callsManager,
+ lock, null, contactsAsyncHelper,
+ callerInfoAsyncQueryFactory, null, null, null, null,
+ Call.CALL_DIRECTION_INCOMING, false, false);
call.setDisconnectCause(
new DisconnectCause(DisconnectCause.MISSED));
call.setState(CallState.DISCONNECTED, "throw away call");
@@ -487,7 +625,7 @@
this); // No longer need to listen to call
// changes after the contact info
// is retrieved.
- showMissedCallNotification(call);
+ showMissedCallNotification(call, userHandle);
}
});
// Set the handle here because that is what triggers the contact
@@ -508,8 +646,24 @@
where.append(" AND new=1");
where.append(" AND is_read=0");
+ Uri callsUri =
+ ContentProvider.maybeAddUserId(Calls.CONTENT_URI, userHandle.getIdentifier());
// start the query
- queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
+ queryHandler.startQuery(0, null, callsUri, CALL_LOG_PROJECTION,
where.toString(), null, Calls.DEFAULT_SORT_ORDER);
}
+
+ @Override
+ public void setCurrentUserHandle(UserHandle currentUserHandle) {
+ mCurrentUserHandle = currentUserHandle;
+ }
+
+ private Context getContextForUser(UserHandle user) {
+ try {
+ return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
+ } catch (NameNotFoundException e) {
+ // Default to mContext, not finding the package system is running as is unlikely.
+ return mContext;
+ }
+ }
}
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index df333a5..e645123 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -18,6 +18,10 @@
coreApp="true"
package="com.android.server.telecom.testapps">
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="23" />
+
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
@@ -50,6 +54,7 @@
<service android:name="com.android.server.telecom.testapps.TestInCallServiceImpl"
android:process="com.android.server.telecom.testapps.TestInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE" >
+ <meta-data android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI" android:value="true" />
<intent-filter>
<action android:name="android.telecom.InCallService"/>
</intent-filter>
@@ -64,6 +69,17 @@
</intent-filter>
</receiver>
+ <activity android:name="com.android.server.telecom.testapps.TestInCallUI"
+ android:process="com.android.server.telecom.testapps.TestInCallService"
+ android:label="@string/inCallUiAppLabel"
+ android:launchMode="singleInstance">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
<activity android:name="com.android.server.telecom.testapps.TestCallActivity"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/testCallActivityLabel">
diff --git a/testapps/res/layout/call_list_item.xml b/testapps/res/layout/call_list_item.xml
new file mode 100644
index 0000000..c9f2ff7
--- /dev/null
+++ b/testapps/res/layout/call_list_item.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="wrap_content"
+ android:orientation="vertical" >
+ <TextView
+ android:id="@+id/phoneNumber"
+ android:layout_gravity="left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+ <TextView
+ android:id="@+id/callState"
+ android:layout_gravity="left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+ <TextView
+ android:id="@+id/duration"
+ android:layout_gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+</LinearLayout>
diff --git a/testapps/res/layout/incall_screen.xml b/testapps/res/layout/incall_screen.xml
new file mode 100644
index 0000000..6a891e7
--- /dev/null
+++ b/testapps/res/layout/incall_screen.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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" >
+ <ListView
+ android:id="@+id/callListView"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="#FFCC00"
+ android:dividerHeight="4px">
+ </ListView>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/end_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/endCallButton" />
+ <Button
+ android:id="@+id/mute_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/muteButton" />
+ <Button
+ android:id="@+id/hold_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/holdButton" >
+ </Button>
+ </LinearLayout>
+</LinearLayout>
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 43c302d..599d5cc 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -37,4 +37,12 @@
<!-- String for button in TestDialerActivity that tries to exercise the
TelecomManager.cancelMissedCallNotifications() functionality -->
<string name="cancelMissedButton">Cancel missed calls</string>
+
+ <string name="endCallButton">End Call</string>
+
+ <string name="muteButton">Mute</string>
+
+ <string name="holdButton">Hold</string>
+
+ <string name="inCallUiAppLabel">Test InCall UI</string>
</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
new file mode 100644
index 0000000..bea0e63
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallListAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2015 Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.telecom.Call;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+public class CallListAdapter extends BaseAdapter {
+ private static final String TAG = "CallListAdapter";
+
+ private final TestCallList.Listener mListener = new TestCallList.Listener() {
+ @Override
+ public void onCallAdded(Call call) {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ notifyDataSetChanged();
+ if (mCallList.size() == 0) {
+ mCallList.removeListener(this);
+ }
+ }
+ };
+
+ private final LayoutInflater mLayoutInflater;
+ private final TestCallList mCallList;
+ private final Handler mHandler = new Handler();
+ private final Runnable mSecondsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ notifyDataSetChanged();
+ if (mCallList.size() > 0) {
+ mHandler.postDelayed(this, 1000);
+ }
+ }
+ };
+
+ public CallListAdapter(Context context) {
+ mLayoutInflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mCallList = TestCallList.getInstance();
+ mCallList.addListener(mListener);
+ mHandler.postDelayed(mSecondsRunnable, 1000);
+ }
+
+
+ @Override
+ public int getCount() {
+ Log.i(TAG, "size reporting: " + mCallList.size());
+ return mCallList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return position;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ Log.i(TAG, "getView: " + position);
+ if (convertView == null) {
+ convertView = mLayoutInflater.inflate(R.layout.call_list_item, parent, false);
+ }
+
+ TextView phoneNumber = (TextView) convertView.findViewById(R.id.phoneNumber);
+ TextView duration = (TextView) convertView.findViewById(R.id.duration);
+ TextView state = (TextView) convertView.findViewById(R.id.callState);
+
+ Call call = mCallList.getCall(position);
+ Uri handle = call.getDetails().getHandle();
+ phoneNumber.setText(handle == null ? "No number" : handle.getSchemeSpecificPart());
+
+ long durationMs = System.currentTimeMillis() - call.getDetails().getConnectTimeMillis();
+ duration.setText((durationMs / 1000) + " secs");
+
+ state.setText(getStateString(call));
+
+ Log.i(TAG, "Call found: " + handle.getSchemeSpecificPart() + ", " + durationMs);
+
+ return convertView;
+ }
+
+ private static String getStateString(Call call) {
+ switch (call.getState()) {
+ case Call.STATE_ACTIVE:
+ return "active";
+ case Call.STATE_CONNECTING:
+ return "connecting";
+ case Call.STATE_DIALING:
+ return "dialing";
+ case Call.STATE_DISCONNECTED:
+ return "disconnected";
+ case Call.STATE_DISCONNECTING:
+ return "disconnecting";
+ case Call.STATE_HOLDING:
+ return "on hold";
+ case Call.STATE_NEW:
+ return "new";
+ case Call.STATE_RINGING:
+ return "ringing";
+ case Call.STATE_SELECT_PHONE_ACCOUNT:
+ return "select phone account";
+ default:
+ return "unknown";
+ }
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
index 20a0475..aee5514 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -25,6 +25,7 @@
import android.support.v4.content.LocalBroadcastManager;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
import android.util.Log;
/**
@@ -44,8 +45,10 @@
"com.android.server.telecom.testapps.ACTION_REGISTER_PHONE_ACCOUNT";
static final String ACTION_SHOW_ALL_PHONE_ACCOUNTS =
"com.android.server.telecom.testapps.ACTION_SHOW_ALL_PHONE_ACCOUNTS";
- static final String ACTION_VIDEO_CALL =
- "com.android.server.telecom.testapps.ACTION_VIDEO_CALL";
+ static final String ACTION_ONE_WAY_VIDEO_CALL =
+ "com.android.server.telecom.testapps.ACTION_ONE_WAY_VIDEO_CALL";
+ static final String ACTION_TWO_WAY_VIDEO_CALL =
+ "com.android.server.telecom.testapps.ACTION_TWO_WAY_VIDEO_CALL";
static final String ACTION_AUDIO_CALL =
"com.android.server.telecom.testapps.ACTION_AUDIO_CALL";
@@ -59,10 +62,12 @@
CallServiceNotifier.getInstance().registerPhoneAccount(context);
} else if (ACTION_SHOW_ALL_PHONE_ACCOUNTS.equals(action)) {
CallServiceNotifier.getInstance().showAllPhoneAccounts(context);
- } else if (ACTION_VIDEO_CALL.equals(action)) {
- sendIncomingCallIntent(context, null, true);
+ } else if (ACTION_ONE_WAY_VIDEO_CALL.equals(action)) {
+ sendIncomingCallIntent(context, null, VideoProfile.STATE_RX_ENABLED);
+ } else if (ACTION_TWO_WAY_VIDEO_CALL.equals(action)) {
+ sendIncomingCallIntent(context, null, VideoProfile.STATE_BIDIRECTIONAL);
} else if (ACTION_AUDIO_CALL.equals(action)) {
- sendIncomingCallIntent(context, null, false);
+ sendIncomingCallIntent(context, null, VideoProfile.STATE_AUDIO_ONLY);
}
}
@@ -70,9 +75,9 @@
* Creates and sends the intent to add an incoming call through Telecom.
*
* @param context The current context.
- * @param isVideoCall {@code True} if this is a video call.
+ * @param videoState The video state requested for the incoming call.
*/
- public static void sendIncomingCallIntent(Context context, Uri handle, boolean isVideoCall) {
+ public static void sendIncomingCallIntent(Context context, Uri handle, int videoState) {
PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
new ComponentName(context, TestConnectionService.class),
CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -80,7 +85,7 @@
// For the purposes of testing, indicate whether the incoming call is a video call by
// stashing an indicator in the EXTRA_INCOMING_CALL_EXTRAS.
Bundle extras = new Bundle();
- extras.putBoolean(TestConnectionService.EXTRA_IS_VIDEO_CALL, isVideoCall);
+ extras.putInt(TestConnectionService.EXTRA_START_VIDEO_STATE, videoState);
if (handle != null) {
extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
}
diff --git a/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
index c1ced80..9292273 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallServiceNotifier.java
@@ -24,11 +24,10 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.net.Uri;
+import android.os.Bundle;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -103,6 +102,14 @@
telecomManager.clearAccounts();
+ Bundle testBundle = new Bundle();
+ testBundle.putInt("EXTRA_INT_1", 1);
+ testBundle.putInt("EXTRA_INT_100", 100);
+ testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+ testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+ testBundle.putString("EXTRA_STR1", "Hello");
+ testBundle.putString("EXTRA_STR2", "There");
+
telecomManager.registerPhoneAccount(PhoneAccount.builder(
new PhoneAccountHandle(
new ComponentName(context, TestConnectionService.class),
@@ -112,13 +119,14 @@
.setSubscriptionAddress(Uri.parse("tel:555-TEST"))
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
PhoneAccount.CAPABILITY_VIDEO_CALLING |
- PhoneAccount.CAPABILITY_CALL_SUBJECT)
+ PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)
.setIcon(Icon.createWithResource(
context.getResources(), R.drawable.stat_sys_phone_call))
- // TODO: Add icon tint (Color.RED)
.setHighlightColor(Color.RED)
+ // TODO: Add icon tint (Color.RED)
.setShortDescription("a short description for the call provider")
.setSupportedUriSchemes(Arrays.asList("tel"))
+ .setExtras(testBundle)
.build());
telecomManager.registerPhoneAccount(PhoneAccount.builder(
@@ -131,11 +139,11 @@
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
PhoneAccount.CAPABILITY_VIDEO_CALLING |
- PhoneAccount.CAPABILITY_CALL_SUBJECT)
+ PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)
.setIcon(Icon.createWithResource(
context.getResources(), R.drawable.stat_sys_phone_call))
- // TODO: Add icon tint (Color.GREEN)
.setHighlightColor(Color.GREEN)
+ // TODO: Add icon tint (Color.GREEN)
.setShortDescription("a short description for the sim subscription")
.build());
@@ -208,7 +216,8 @@
builder.setContentText("Test calls via CallService API");
builder.setContentTitle("Test Connection Service");
- addAddVideoCallAction(builder, context);
+ addAddOneWayVideoCallAction(builder, context);
+ addAddTwoWayVideoCallAction(builder, context);
addAddCallAction(builder, context);
addExitAction(builder, context);
@@ -244,10 +253,19 @@
}
/**
- * Creates the intent to start an incoming video call
+ * Creates the intent to start an incoming 1-way video call (receive-only)
*/
- private PendingIntent createIncomingVideoCall(Context context) {
- final Intent intent = new Intent(CallNotificationReceiver.ACTION_VIDEO_CALL,
+ private PendingIntent createOneWayVideoCallIntent(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_ONE_WAY_VIDEO_CALL,
+ null, context, CallNotificationReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /**
+ * Creates the intent to start an incoming 2-way video call
+ */
+ private PendingIntent createTwoWayVideoCallIntent(Context context) {
+ final Intent intent = new Intent(CallNotificationReceiver.ACTION_TWO_WAY_VIDEO_CALL,
null, context, CallNotificationReceiver.class);
return PendingIntent.getBroadcast(context, 0, intent, 0);
}
@@ -270,10 +288,19 @@
}
/**
- * Adds an action to the Notification Builder to add an incoming video call through Telecom.
+ * Adds an action to the Notification Builder to add an incoming one-way video call through
+ * Telecom.
*/
- private void addAddVideoCallAction(Notification.Builder builder, Context context) {
- builder.addAction(0, "Add Video", createIncomingVideoCall(context));
+ private void addAddOneWayVideoCallAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "1-way Video", createOneWayVideoCallIntent(context));
+ }
+
+ /**
+ * Adds an action to the Notification Builder to add an incoming 2-way video call through
+ * Telecom.
+ */
+ private void addAddTwoWayVideoCallAction(Notification.Builder builder, Context context) {
+ builder.addAction(0, "2-way Video", createTwoWayVideoCallIntent(context));
}
/**
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
index 4ac151f..862ccf7 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallActivity.java
@@ -20,6 +20,7 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.telecom.VideoProfile;
/**
* This activity exists in order to add an icon to the launcher. This activity has no UI of its own
@@ -58,7 +59,8 @@
final String action = intent != null ? intent.getAction() : null;
final Uri data = intent != null ? intent.getData() : null;
if (ACTION_NEW_INCOMING_CALL.equals(action) && data != null) {
- CallNotificationReceiver.sendIncomingCallIntent(this, data, false);
+ CallNotificationReceiver.sendIncomingCallIntent(this, data,
+ VideoProfile.STATE_AUDIO_ONLY);
} else if (ACTION_NEW_UNKNOWN_CALL.equals(action) && data != null) {
CallNotificationReceiver.addNewUnknownCall(this, data, intent.getExtras());
} else if (ACTION_HANGUP_CALLS.equals(action)) {
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallList.java b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
index a16c4e2..704c83d 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
@@ -24,6 +24,8 @@
import android.util.ArraySet;
import android.util.Log;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -31,6 +33,12 @@
* Maintains a list of calls received via the {@link TestInCallServiceImpl}.
*/
public class TestCallList extends Call.Listener {
+
+ public static abstract class Listener {
+ public void onCallAdded(Call call) {}
+ public void onCallRemoved(Call call) {}
+ }
+
private static final TestCallList INSTANCE = new TestCallList();
private static final String TAG = "TestCallList";
@@ -85,9 +93,10 @@
}
// The calls the call list knows about.
- private Set<Call> mCalls = new ArraySet<Call>();
+ private List<Call> mCalls = new LinkedList<Call>();
private Map<Call, TestVideoCallListener> mVideoCallListeners =
new ArrayMap<Call, TestVideoCallListener>();
+ private Set<Listener> mListeners = new ArraySet<Listener>();
/**
* Singleton accessor.
@@ -96,14 +105,32 @@
return INSTANCE;
}
+ public void addListener(Listener listener) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ }
+
+ public boolean removeListener(Listener listener) {
+ return mListeners.remove(listener);
+ }
+
+ public Call getCall(int position) {
+ return mCalls.get(position);
+ }
+
public void addCall(Call call) {
if (mCalls.contains(call)) {
Log.e(TAG, "addCall: Call already added.");
return;
}
- Log.v(TAG, "addCall: " + call + " " + System.identityHashCode(this));
+ Log.i(TAG, "addCall: " + call + " " + System.identityHashCode(this));
mCalls.add(call);
call.addListener(this);
+
+ for (Listener l : mListeners) {
+ l.onCallAdded(call);
+ }
}
public void removeCall(Call call) {
@@ -111,9 +138,13 @@
Log.e(TAG, "removeCall: Call cannot be removed -- doesn't exist.");
return;
}
- Log.v(TAG, "removeCall: " + call);
+ Log.i(TAG, "removeCall: " + call);
mCalls.remove(call);
call.removeListener(this);
+
+ for (Listener l : mListeners) {
+ l.onCallRemoved(call);
+ }
}
public void clearCalls() {
@@ -126,6 +157,10 @@
mVideoCallListeners.clear();
}
+ public int size() {
+ return mCalls.size();
+ }
+
/**
* For any video calls tracked, sends an upgrade to video request.
*/
diff --git a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
index 7964355..c819838 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestConnectionService.java
@@ -51,10 +51,9 @@
*/
public class TestConnectionService extends ConnectionService {
/**
- * Intent extra used to pass along whether a call is video or audio based on the user's choice
- * in the notification.
+ * Intent extra used to pass along the video state for a new test call.
*/
- public static final String EXTRA_IS_VIDEO_CALL = "extra_is_video_call";
+ public static final String EXTRA_START_VIDEO_STATE = "extra_start_video_state";
public static final String EXTRA_HANDLE = "extra_handle";
@@ -350,17 +349,14 @@
final TestConnection connection = new TestConnection(true);
// Get the stashed intent extra that determines if this is a video call or audio call.
Bundle extras = request.getExtras();
- boolean isVideoCall = extras.getBoolean(EXTRA_IS_VIDEO_CALL);
+ int videoState = extras.getInt(EXTRA_START_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
// Use dummy number for testing incoming calls.
Uri address = providedHandle == null ?
- Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(isVideoCall), null)
+ Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(
+ VideoProfile.isVideo(videoState)), null)
: providedHandle;
-
- int videoState = isVideoCall ?
- VideoProfile.STATE_BIDIRECTIONAL :
- VideoProfile.STATE_AUDIO_ONLY;
connection.setVideoState(videoState);
Bundle connectionExtras = connection.getExtras();
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
index 68bbac9..03ca3d0 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallServiceImpl.java
@@ -16,6 +16,8 @@
package com.android.server.telecom.testapps;
+import android.content.Context;
+import android.content.Intent;
import android.telecom.Call;
import android.telecom.InCallService;
import android.telecom.Phone;
@@ -37,7 +39,12 @@
@Override
public void onCallAdded(Phone phone, Call call) {
Log.i(TAG, "onCallAdded: " + call.toString());
- TestCallList.getInstance().addCall(call);
+ TestCallList callList = TestCallList.getInstance();
+ callList.addCall(call);
+
+ if (callList.size() == 1) {
+ startInCallUI();
+ }
}
@Override
@@ -62,4 +69,11 @@
mPhone = null;
TestCallList.getInstance().clearCalls();
}
+
+ private void startInCallUI() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClass(this, TestInCallUI.class);
+ startActivity(intent);
+ }
}
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
new file mode 100644
index 0000000..ce53709
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ListView;
+
+public class TestInCallUI extends Activity {
+
+ private ListView mListView;
+ private TestCallList mCallList;
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.incall_screen);
+
+ mListView = (ListView) findViewById(R.id.callListView);
+ mListView.setAdapter(new CallListAdapter(this));
+ mListView.setVisibility(View.VISIBLE);
+
+ mCallList = TestCallList.getInstance();
+ mCallList.addListener(new TestCallList.Listener() {
+ @Override
+ public void onCallRemoved(Call call) {
+ if (mCallList.size() == 0) {
+ Log.i("Santos", "Ending the incall UI");
+ finish();
+ }
+ }
+ });
+
+ View endCallButton = findViewById(R.id.end_call_button);
+ View holdButton = findViewById(R.id.hold_button);
+ View muteButton = findViewById(R.id.mute_button);
+
+ endCallButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Call call = mCallList.getCall(0);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ });
+ holdButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Call call = mCallList.getCall(0);
+ if (call != null) {
+ if (call.getState() == Call.STATE_HOLDING) {
+ call.unhold();
+ } else {
+ call.hold();
+ }
+ }
+ }
+ });
+ muteButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Call call = mCallList.getCall(0);
+ if (call != null) {
+ }
+ }
+ });
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index a802768..317bfe3 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -39,6 +39,9 @@
--auto-add-overlay \
--extra-packages com.android.server.telecom
+LOCAL_JACK_ENABLED := disabled
+LOCAL_PROGUARD_ENABLED := disabled
+
LOCAL_PACKAGE_NAME := TelecomUnitTests
LOCAL_CERTIFICATE := platform
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 46056ae..f84a545 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -19,6 +19,10 @@
package="com.android.server.telecom.tests"
android:debuggable="true">
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="23" />
+
<!-- TODO: Needed because we call BluetoothAdapter.getDefaultAdapter() statically, and
BluetoothAdapter is a final class. -->
<uses-permission android:name="android.permission.BLUETOOTH" />
@@ -43,8 +47,8 @@
adb shell am instrument -w com.android.server.telecom.tests/android.test.InstrumentationTestRunner
To run a single test case:
- adb shell am instrument -w com.android.server.telecom.tests/android.test.InstrumentationTestRunner
- -e com.android.server.telecom.tests.unit.FooUnitTest
+ adb shell am instrument -w -e class com.android.server.telecom.tests.unit.FooUnitTest \
+ com.android.server.telecom.tests/android.test.InstrumentationTestRunner
-->
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.server.telecom.tests"
diff --git a/tests/res/drawable-xhdpi/contacts_sample_photo.png b/tests/res/drawable-xhdpi/contacts_sample_photo.png
new file mode 100644
index 0000000..6c0ba64
--- /dev/null
+++ b/tests/res/drawable-xhdpi/contacts_sample_photo.png
Binary files differ
diff --git a/tests/res/drawable-xhdpi/contacts_sample_photo_small.png b/tests/res/drawable-xhdpi/contacts_sample_photo_small.png
new file mode 100644
index 0000000..f53602e
--- /dev/null
+++ b/tests/res/drawable-xhdpi/contacts_sample_photo_small.png
Binary files differ
diff --git a/tests/res/values/dimens.xml b/tests/res/values/dimens.xml
new file mode 100644
index 0000000..031f5b4
--- /dev/null
+++ b/tests/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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>
+ <dimen name="notification_icon_size">64dp</dimen>
+</resources>
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
new file mode 100644
index 0000000..e7b76ac
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.Log;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Process;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.ParcelableCall;
+import android.telecom.ParcelableCallAnalytics;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telecom.IInCallAdapter;
+
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Performs various basic call tests in Telecom.
+ */
+public class BasicCallTests extends TelecomSystemTest {
+ @LargeTest
+ public void testSingleOutgoingCallLocalDisconnect() throws Exception {
+ IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
+ assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+ mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+ }
+
+ @LargeTest
+ public void testSingleOutgoingCallRemoteDisconnect() throws Exception {
+ IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+ }
+
+ /**
+ * Tests the {@link TelecomManager#acceptRingingCall()} API. Tests simple case of an incoming
+ * audio-only call.
+ *
+ * @throws Exception
+ */
+ @LargeTest
+ public void testTelecomManagerAcceptRingingCall() throws Exception {
+ IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
+ mConnectionServiceFixtureA);
+
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+ // Use TelecomManager API to answer the ringing call.
+ TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
+ .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
+ telecomManager.acceptRingingCall();
+
+ verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+ .answer(ids.mCallId);
+ mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
+
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
+ }
+
+ /**
+ * Tests the {@link TelecomManager#acceptRingingCall()} API. Tests simple case of an incoming
+ * video call, which should be answered as video.
+ *
+ * @throws Exception
+ */
+ @LargeTest
+ public void testTelecomManagerAcceptRingingVideoCall() throws Exception {
+ IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+ // Use TelecomManager API to answer the ringing call; the default expected behavior is to
+ // answer using whatever video state the ringing call requests.
+ TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
+ .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
+ telecomManager.acceptRingingCall();
+
+ // Answer video API should be called
+ verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+ .answerVideo(eq(ids.mCallId), eq(VideoProfile.STATE_BIDIRECTIONAL));
+ mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
+
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
+ }
+
+ /**
+ * Tests the {@link TelecomManager#acceptRingingCall(int)} API. Tests answering a video call
+ * as an audio call.
+ *
+ * @throws Exception
+ */
+ @LargeTest
+ public void testTelecomManagerAcceptRingingVideoCallAsAudio() throws Exception {
+ IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+ // Use TelecomManager API to answer the ringing call.
+ TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
+ .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
+ telecomManager.acceptRingingCall(VideoProfile.STATE_AUDIO_ONLY);
+
+ // The generic answer method on the ConnectionService is used to answer audio-only calls.
+ verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+ .answer(eq(ids.mCallId));
+ mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
+
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
+ }
+
+ /**
+ * Tests the {@link TelecomManager#acceptRingingCall()} API. Tests simple case of an incoming
+ * video call, where an attempt is made to answer with an invalid video state.
+ *
+ * @throws Exception
+ */
+ @LargeTest
+ public void testTelecomManagerAcceptRingingInvalidVideoState() throws Exception {
+ IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
+ VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
+
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+ // Use TelecomManager API to answer the ringing call; the default expected behavior is to
+ // answer using whatever video state the ringing call requests.
+ TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
+ .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
+ telecomManager.acceptRingingCall(999 /* invalid videostate */);
+
+ // Answer video API should be called
+ verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+ .answerVideo(eq(ids.mCallId), eq(VideoProfile.STATE_BIDIRECTIONAL));
+ mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
+ }
+
+ @LargeTest
+ public void testSingleIncomingCallLocalDisconnect() throws Exception {
+ IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
+ assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+
+ mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+ }
+
+ @LargeTest
+ public void testSingleIncomingCallRemoteDisconnect() throws Exception {
+ IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+ assertEquals(Call.STATE_DISCONNECTED,
+ mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+ }
+
+ public void do_testDeadlockOnOutgoingCall() throws Exception {
+ final IdPair ids = startOutgoingPhoneCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ Process.myUserHandle());
+ rapidFire(
+ new Runnable() {
+ @Override
+ public void run() {
+ while (mCallerInfoAsyncQueryFactoryFixture.mRequests.size() > 0) {
+ mCallerInfoAsyncQueryFactoryFixture.mRequests.remove(0).reply();
+ }
+ }
+ },
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
+ } catch (Exception e) {
+ Log.e(this, e, "");
+ }
+ }
+ });
+ }
+
+ @MediumTest
+ public void testDeadlockOnOutgoingCall() throws Exception {
+ for (int i = 0; i < 100; i++) {
+ BasicCallTests test = new BasicCallTests();
+ test.setContext(getContext());
+ test.setTestContext(getTestContext());
+ test.setName(getName());
+ test.setUp();
+ test.do_testDeadlockOnOutgoingCall();
+ test.tearDown();
+ }
+ }
+
+ @LargeTest
+ public void testIncomingThenOutgoingCalls() throws Exception {
+ // TODO: We have to use the same PhoneAccount for both; see http://b/18461539
+ IdPair incoming = startAndMakeActiveIncomingCall("650-555-2323",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(incoming.mCallId);
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(outgoing.mCallId);
+ }
+
+ @LargeTest
+ public void testOutgoingThenIncomingCalls() throws Exception {
+ // TODO: We have to use the same PhoneAccount for both; see http://b/18461539
+ IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ IdPair incoming = startAndMakeActiveIncomingCall("650-555-2323",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ verify(mConnectionServiceFixtureA.getTestDouble())
+ .hold(outgoing.mConnectionId);
+ mConnectionServiceFixtureA.mConnectionById.get(outgoing.mConnectionId).state =
+ Connection.STATE_HOLDING;
+ mConnectionServiceFixtureA.sendSetOnHold(outgoing.mConnectionId);
+ assertEquals(Call.STATE_HOLDING,
+ mInCallServiceFixtureX.getCall(outgoing.mCallId).getState());
+ assertEquals(Call.STATE_HOLDING,
+ mInCallServiceFixtureY.getCall(outgoing.mCallId).getState());
+
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(incoming.mCallId);
+ mInCallServiceFixtureX.mInCallAdapter.disconnectCall(outgoing.mCallId);
+ }
+
+ public void testAudioManagerOperations() throws Exception {
+ AudioManager audioManager = (AudioManager) mComponentContextFixture.getTestDouble()
+ .getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+
+ IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ verify(audioManager, timeout(TEST_TIMEOUT)).requestAudioFocusForCall(anyInt(), anyInt());
+ verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce())
+ .setMode(AudioManager.MODE_IN_CALL);
+
+ mInCallServiceFixtureX.mInCallAdapter.mute(true);
+ verify(mAudioService, timeout(TEST_TIMEOUT))
+ .setMicrophoneMute(eq(true), any(String.class), any(Integer.class));
+ mInCallServiceFixtureX.mInCallAdapter.mute(false);
+ verify(mAudioService, timeout(TEST_TIMEOUT))
+ .setMicrophoneMute(eq(false), any(String.class), any(Integer.class));
+
+ mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
+ verify(audioManager, timeout(TEST_TIMEOUT))
+ .setSpeakerphoneOn(true);
+ mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE);
+ verify(audioManager, timeout(TEST_TIMEOUT))
+ .setSpeakerphoneOn(false);
+
+ mConnectionServiceFixtureA.
+ sendSetDisconnected(outgoing.mConnectionId, DisconnectCause.REMOTE);
+
+ verify(audioManager, timeout(TEST_TIMEOUT))
+ .abandonAudioFocusForCall();
+ verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce())
+ .setMode(AudioManager.MODE_NORMAL);
+ }
+
+ private void rapidFire(Runnable... tasks) {
+ final CyclicBarrier barrier = new CyclicBarrier(tasks.length);
+ final CountDownLatch latch = new CountDownLatch(tasks.length);
+ for (int i = 0; i < tasks.length; i++) {
+ final Runnable task = tasks[i];
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ barrier.await();
+ task.run();
+ } catch (InterruptedException | BrokenBarrierException e){
+ Log.e(BasicCallTests.this, e, "Unexpectedly interrupted");
+ } finally {
+ latch.countDown();
+ }
+ }
+ }).start();
+ }
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Log.e(BasicCallTests.this, e, "Unexpectedly interrupted");
+ }
+ }
+
+ @MediumTest
+ public void testBasicConferenceCall() throws Exception {
+ makeConferenceCall();
+ }
+
+ @MediumTest
+ public void testAddCallToConference1() throws Exception {
+ ParcelableCall conferenceCall = makeConferenceCall();
+ IdPair callId3 = startAndMakeActiveOutgoingCall("650-555-1214",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ // testAddCallToConference{1,2} differ in the order of arguments to InCallAdapter#conference
+ mInCallServiceFixtureX.getInCallAdapter().conference(
+ conferenceCall.getId(), callId3.mCallId);
+ Thread.sleep(200);
+
+ ParcelableCall call3 = mInCallServiceFixtureX.getCall(callId3.mCallId);
+ ParcelableCall updatedConference = mInCallServiceFixtureX.getCall(conferenceCall.getId());
+ assertEquals(conferenceCall.getId(), call3.getParentCallId());
+ assertEquals(3, updatedConference.getChildCallIds().size());
+ assertTrue(updatedConference.getChildCallIds().contains(callId3.mCallId));
+ }
+
+ @MediumTest
+ public void testAddCallToConference2() throws Exception {
+ ParcelableCall conferenceCall = makeConferenceCall();
+ IdPair callId3 = startAndMakeActiveOutgoingCall("650-555-1214",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+ mInCallServiceFixtureX.getInCallAdapter()
+ .conference(callId3.mCallId, conferenceCall.getId());
+ Thread.sleep(200);
+
+ ParcelableCall call3 = mInCallServiceFixtureX.getCall(callId3.mCallId);
+ ParcelableCall updatedConference = mInCallServiceFixtureX.getCall(conferenceCall.getId());
+ assertEquals(conferenceCall.getId(), call3.getParentCallId());
+ assertEquals(3, updatedConference.getChildCallIds().size());
+ assertTrue(updatedConference.getChildCallIds().contains(callId3.mCallId));
+ }
+
+ private ParcelableCall makeConferenceCall() throws Exception {
+ IdPair callId1 = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ IdPair callId2 = startAndMakeActiveOutgoingCall("650-555-1213",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+
+ IInCallAdapter inCallAdapter = mInCallServiceFixtureX.getInCallAdapter();
+ inCallAdapter.conference(callId1.mCallId, callId2.mCallId);
+ // Wait for wacky non-deterministic behavior
+ Thread.sleep(200);
+ ParcelableCall call1 = mInCallServiceFixtureX.getCall(callId1.mCallId);
+ ParcelableCall call2 = mInCallServiceFixtureX.getCall(callId2.mCallId);
+ // Check that the two calls end up with a parent in the end
+ assertNotNull(call1.getParentCallId());
+ assertNotNull(call2.getParentCallId());
+ assertEquals(call1.getParentCallId(), call2.getParentCallId());
+
+ // Check to make sure that the parent call made it to the in-call service
+ String parentCallId = call1.getParentCallId();
+ ParcelableCall conferenceCall = mInCallServiceFixtureX.getCall(parentCallId);
+ assertEquals(2, conferenceCall.getChildCallIds().size());
+ assertTrue(conferenceCall.getChildCallIds().contains(callId1.mCallId));
+ assertTrue(conferenceCall.getChildCallIds().contains(callId2.mCallId));
+ return conferenceCall;
+ }
+
+ @MediumTest
+ public void testAnalyticsSingleCall() throws Exception {
+ IdPair testCall = startAndMakeActiveIncomingCall(
+ "650-555-1212",
+ mPhoneAccountA0.getAccountHandle(),
+ mConnectionServiceFixtureA);
+ Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+
+ assertTrue(analyticsMap.containsKey(testCall.mCallId));
+
+ Analytics.CallInfoImpl callAnalytics = analyticsMap.get(testCall.mCallId);
+ assertTrue(callAnalytics.startTime > 0);
+ assertEquals(0, callAnalytics.endTime);
+ assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics.callDirection);
+ assertFalse(callAnalytics.isInterrupted);
+ assertNull(callAnalytics.callTerminationReason);
+ assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
+ callAnalytics.connectionService);
+
+ mConnectionServiceFixtureA.
+ sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
+
+ analyticsMap = Analytics.cloneData();
+ callAnalytics = analyticsMap.get(testCall.mCallId);
+ assertTrue(callAnalytics.endTime > 0);
+ assertNotNull(callAnalytics.callTerminationReason);
+ assertEquals(DisconnectCause.ERROR, callAnalytics.callTerminationReason.getCode());
+
+ StringWriter sr = new StringWriter();
+ IndentingPrintWriter ip = new IndentingPrintWriter(sr, " ");
+ Analytics.dump(ip);
+ String dumpResult = sr.toString();
+ String[] expectedFields = {"startTime", "endTime", "direction", "isAdditionalCall",
+ "isInterrupted", "callTechnologies", "callTerminationReason", "connectionService"};
+ for (String field : expectedFields) {
+ assertTrue(dumpResult.contains(field));
+ }
+ }
+
+ @SmallTest
+ public void testAnalyticsDumping() throws Exception {
+ Analytics.reset();
+ IdPair testCall = startAndMakeActiveIncomingCall(
+ "650-555-1212",
+ mPhoneAccountA0.getAccountHandle(),
+ mConnectionServiceFixtureA);
+
+ mConnectionServiceFixtureA.
+ sendSetDisconnected(testCall.mConnectionId, DisconnectCause.ERROR);
+ Analytics.CallInfoImpl expectedAnalytics = Analytics.cloneData().get(testCall.mCallId);
+
+ TelecomManager tm = (TelecomManager) mSpyContext.getSystemService(Context.TELECOM_SERVICE);
+ List<ParcelableCallAnalytics> analyticsList = tm.dumpAnalytics();
+
+ assertEquals(1, analyticsList.size());
+ ParcelableCallAnalytics pCA = analyticsList.get(0);
+
+ assertTrue(Math.abs(expectedAnalytics.startTime - pCA.getStartTimeMillis()) <
+ ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+ assertEquals(0, pCA.getStartTimeMillis() % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
+ assertTrue(Math.abs((expectedAnalytics.endTime - expectedAnalytics.startTime) -
+ pCA.getCallDurationMillis()) < ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
+ assertEquals(0, pCA.getCallDurationMillis() % ParcelableCallAnalytics.MILLIS_IN_1_SECOND);
+
+ assertEquals(expectedAnalytics.callDirection, pCA.getCallType());
+ assertEquals(expectedAnalytics.isAdditionalCall, pCA.isAdditionalCall());
+ assertEquals(expectedAnalytics.isInterrupted, pCA.isInterrupted());
+ assertEquals(expectedAnalytics.callTechnologies, pCA.getCallTechnologies());
+ assertEquals(expectedAnalytics.callTerminationReason.getCode(),
+ pCA.getCallTerminationCode());
+ assertEquals(expectedAnalytics.connectionService, pCA.getConnectionService());
+ }
+
+ @MediumTest
+ public void testAnalyticsTwoCalls() throws Exception {
+ IdPair testCall1 = startAndMakeActiveIncomingCall(
+ "650-555-1212",
+ mPhoneAccountA0.getAccountHandle(),
+ mConnectionServiceFixtureA);
+ IdPair testCall2 = startAndMakeActiveOutgoingCall(
+ "650-555-1213",
+ mPhoneAccountA0.getAccountHandle(),
+ mConnectionServiceFixtureA);
+
+ Map<String, Analytics.CallInfoImpl> analyticsMap = Analytics.cloneData();
+ assertTrue(analyticsMap.containsKey(testCall1.mCallId));
+ assertTrue(analyticsMap.containsKey(testCall2.mCallId));
+
+ Analytics.CallInfoImpl callAnalytics1 = analyticsMap.get(testCall1.mCallId);
+ Analytics.CallInfoImpl callAnalytics2 = analyticsMap.get(testCall2.mCallId);
+ assertTrue(callAnalytics1.startTime > 0);
+ assertTrue(callAnalytics2.startTime > 0);
+ assertEquals(0, callAnalytics1.endTime);
+ assertEquals(0, callAnalytics2.endTime);
+
+ assertEquals(Analytics.INCOMING_DIRECTION, callAnalytics1.callDirection);
+ assertEquals(Analytics.OUTGOING_DIRECTION, callAnalytics2.callDirection);
+
+ assertTrue(callAnalytics1.isInterrupted);
+ assertTrue(callAnalytics2.isAdditionalCall);
+
+ assertNull(callAnalytics1.callTerminationReason);
+ assertNull(callAnalytics2.callTerminationReason);
+
+ assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
+ callAnalytics1.connectionService);
+ assertEquals(mConnectionServiceComponentNameA.flattenToShortString(),
+ callAnalytics1.connectionService);
+
+ mConnectionServiceFixtureA.
+ sendSetDisconnected(testCall2.mConnectionId, DisconnectCause.REMOTE);
+ mConnectionServiceFixtureA.
+ sendSetDisconnected(testCall1.mConnectionId, DisconnectCause.ERROR);
+
+ analyticsMap = Analytics.cloneData();
+ callAnalytics1 = analyticsMap.get(testCall1.mCallId);
+ callAnalytics2 = analyticsMap.get(testCall2.mCallId);
+ assertTrue(callAnalytics1.endTime > 0);
+ assertTrue(callAnalytics2.endTime > 0);
+ assertNotNull(callAnalytics1.callTerminationReason);
+ assertNotNull(callAnalytics2.callTerminationReason);
+ assertEquals(DisconnectCause.ERROR, callAnalytics1.callTerminationReason.getCode());
+ assertEquals(DisconnectCause.REMOTE, callAnalytics2.callTerminationReason.getCode());
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
new file mode 100644
index 0000000..74a2d64
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
@@ -0,0 +1,886 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.bluetooth.BluetoothAdapter;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Debug;
+import android.telecom.Connection;
+import android.telecom.GatewayInfo;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+
+import com.android.server.telecom.BluetoothHeadsetProxy;
+import com.android.server.telecom.BluetoothPhoneServiceImpl;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyChar;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+public class BluetoothPhoneServiceTest extends TelecomTestCase {
+
+ private static final int TEST_DTMF_TONE = 0;
+ private static final String TEST_ACCOUNT_ADDRESS = "//foo.com/";
+ private static final int TEST_ACCOUNT_INDEX = 0;
+
+ // match up with BluetoothPhoneServiceImpl
+ private static final int CALL_STATE_ACTIVE = 0;
+ private static final int CALL_STATE_HELD = 1;
+ private static final int CALL_STATE_DIALING = 2;
+ private static final int CALL_STATE_ALERTING = 3;
+ private static final int CALL_STATE_INCOMING = 4;
+ private static final int CALL_STATE_WAITING = 5;
+ private static final int CALL_STATE_IDLE = 6;
+ // Terminate all held or set UDUB("busy") to a waiting call
+ private static final int CHLD_TYPE_RELEASEHELD = 0;
+ // Terminate all active calls and accepts a waiting/held call
+ private static final int CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD = 1;
+ // Hold all active calls and accepts a waiting/held call
+ private static final int CHLD_TYPE_HOLDACTIVE_ACCEPTHELD = 2;
+ // Add all held calls to a conference
+ private static final int CHLD_TYPE_ADDHELDTOCONF = 3;
+
+ private BluetoothPhoneServiceImpl mBluetoothPhoneService;
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {
+ };
+
+ @Mock CallsManager mMockCallsManager;
+ @Mock PhoneAccountRegistrar mMockPhoneAccountRegistrar;
+ @Mock BluetoothHeadsetProxy mMockBluetoothHeadset;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+
+ // Ensure initialization does not actually try to access any of the CallsManager fields.
+ // This also works to return null if it is not overwritten later in the test.
+ doNothing().when(mMockCallsManager).addListener(any(
+ CallsManager.CallsManagerListener.class));
+ doReturn(null).when(mMockCallsManager).getActiveCall();
+ doReturn(null).when(mMockCallsManager).getRingingCall();
+ doReturn(null).when(mMockCallsManager).getHeldCall();
+ doReturn(null).when(mMockCallsManager).getOutgoingCall();
+ doReturn(0).when(mMockCallsManager).getNumHeldCalls();
+ mBluetoothPhoneService = new BluetoothPhoneServiceImpl(mContext, mLock, mMockCallsManager,
+ mMockPhoneAccountRegistrar);
+
+ // Bring in test Bluetooth Headset
+ mBluetoothPhoneService.setBluetoothHeadset(mMockBluetoothHeadset);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+
+ mBluetoothPhoneService = null;
+ super.tearDown();
+ }
+
+ public void testHeadsetAnswerCall() throws Exception {
+ Call mockCall = createRingingCall();
+
+ boolean callAnswered = mBluetoothPhoneService.mBinder.answerCall();
+
+ verify(mMockCallsManager).answerCall(eq(mockCall), any(int.class));
+ assertEquals(callAnswered, true);
+ }
+
+ public void testHeadsetAnswerCallNull() throws Exception {
+ when(mMockCallsManager.getRingingCall()).thenReturn(null);
+
+ boolean callAnswered = mBluetoothPhoneService.mBinder.answerCall();
+
+ verify(mMockCallsManager,never()).answerCall(any(Call.class), any(int.class));
+ assertEquals(callAnswered, false);
+ }
+
+ public void testHeadsetHangupCall() throws Exception {
+ Call mockCall = createForegroundCall();
+
+ boolean callHungup = mBluetoothPhoneService.mBinder.hangupCall();
+
+ verify(mMockCallsManager).disconnectCall(eq(mockCall));
+ assertEquals(callHungup, true);
+ }
+
+ public void testHeadsetHangupCallNull() throws Exception {
+ when(mMockCallsManager.getForegroundCall()).thenReturn(null);
+
+ boolean callHungup = mBluetoothPhoneService.mBinder.hangupCall();
+
+ verify(mMockCallsManager,never()).disconnectCall(any(Call.class));
+ assertEquals(callHungup, false);
+ }
+
+ public void testHeadsetSendDTMF() throws Exception {
+ Call mockCall = createForegroundCall();
+
+ boolean sentDtmf = mBluetoothPhoneService.mBinder.sendDtmf(TEST_DTMF_TONE);
+
+ verify(mMockCallsManager).playDtmfTone(eq(mockCall), eq((char) TEST_DTMF_TONE));
+ verify(mMockCallsManager).stopDtmfTone(eq(mockCall));
+ assertEquals(sentDtmf, true);
+ }
+
+ public void testHeadsetSendDTMFNull() throws Exception {
+ when(mMockCallsManager.getForegroundCall()).thenReturn(null);
+
+ boolean sentDtmf = mBluetoothPhoneService.mBinder.sendDtmf(TEST_DTMF_TONE);
+
+ verify(mMockCallsManager,never()).playDtmfTone(any(Call.class), anyChar());
+ verify(mMockCallsManager,never()).stopDtmfTone(any(Call.class));
+ assertEquals(sentDtmf, false);
+ }
+
+ public void testGetNetworkOperator() throws Exception {
+ Call mockCall = createForegroundCall();
+ PhoneAccount fakePhoneAccount = makeQuickAccount("id0", TEST_ACCOUNT_INDEX);
+ when(mMockPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+ any(PhoneAccountHandle.class))).thenReturn(fakePhoneAccount);
+
+ String networkOperator = mBluetoothPhoneService.mBinder.getNetworkOperator();
+
+ assertEquals(networkOperator, "label0");
+ }
+
+ public void testGetNetworkOperatorNoPhoneAccount() throws Exception {
+ when(mMockCallsManager.getForegroundCall()).thenReturn(null);
+
+ String networkOperator = mBluetoothPhoneService.mBinder.getNetworkOperator();
+
+ assertEquals(networkOperator, "label1");
+ }
+
+ public void testGetSubscriberNumber() throws Exception {
+ Call mockCall = createForegroundCall();
+ PhoneAccount fakePhoneAccount = makeQuickAccount("id0", TEST_ACCOUNT_INDEX);
+ when(mMockPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+ any(PhoneAccountHandle.class))).thenReturn(fakePhoneAccount);
+
+ String subscriberNumber = mBluetoothPhoneService.mBinder.getSubscriberNumber();
+
+ assertEquals(subscriberNumber, TEST_ACCOUNT_ADDRESS + TEST_ACCOUNT_INDEX);
+ }
+
+ public void testGetSubscriberNumberFallbackToTelephony() throws Exception {
+ Call mockCall = createForegroundCall();
+ String fakeNumber = "8675309";
+ when(mMockPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+ any(PhoneAccountHandle.class))).thenReturn(null);
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(
+ any(PhoneAccountHandle.class))).thenReturn(null);
+ when(TelephonyManager.from(mContext).getLine1Number()).thenReturn(fakeNumber);
+
+ String subscriberNumber = mBluetoothPhoneService.mBinder.getSubscriberNumber();
+
+ assertEquals(subscriberNumber, fakeNumber);
+ }
+
+ public void testListCurrentCallsOneCall() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ Call activeCall = createActiveCall();
+ when(activeCall.getState()).thenReturn(CallState.ACTIVE);
+ calls.add(activeCall);
+ when(activeCall.isConference()).thenReturn(false);
+ when(activeCall.getHandle()).thenReturn(Uri.parse("tel:555-000"));
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+
+ verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(0), eq(0), eq(false),
+ eq("555-000"), eq(PhoneNumberUtils.TOA_Unknown));
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ }
+
+ public void testConferenceInProgressCDMA() throws Exception {
+ // If two calls are being conferenced and updateHeadsetWithCallState runs while this is
+ // still occuring, it will look like there is an active and held call still while we are
+ // transitioning into a conference.
+ // Call has been put into a CDMA "conference" with one call on hold.
+ ArrayList<Call> calls = new ArrayList<>();
+ Call parentCall = createActiveCall();
+ final Call confCall1 = mock(Call.class);
+ final Call confCall2 = createHeldCall();
+ calls.add(parentCall);
+ calls.add(confCall1);
+ calls.add(confCall2);
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ when(confCall1.getState()).thenReturn(CallState.ACTIVE);
+ when(confCall2.getState()).thenReturn(CallState.ACTIVE);
+ when(confCall1.isIncoming()).thenReturn(false);
+ when(confCall2.isIncoming()).thenReturn(true);
+ when(confCall1.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+ when(confCall2.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0001")));
+ addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+ addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+ removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentCall.getConferenceLevelActiveCall()).thenReturn(confCall1);
+ when(parentCall.isConference()).thenReturn(true);
+ when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+ add(confCall1);
+ add(confCall2);
+ }});
+ //Add links from child calls to parent
+ when(confCall1.getParentCall()).thenReturn(parentCall);
+ when(confCall2.getParentCall()).thenReturn(parentCall);
+
+ mBluetoothPhoneService.mBinder.queryPhoneState();
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+ when(parentCall.wasConferencePreviouslyMerged()).thenReturn(true);
+ mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+ when(mMockCallsManager.getHeldCall()).thenReturn(null);
+ // Spurious call to onIsConferencedChanged.
+ mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+ // Make sure the call has only occurred collectively 2 times (not on the third)
+ verify(mMockBluetoothHeadset, times(2)).phoneStateChanged(any(int.class),
+ any(int.class), any(int.class), any(String.class), any(int.class));
+ }
+
+ public void testListCurrentCallsCdmaHold() throws Exception {
+ // Call has been put into a CDMA "conference" with one call on hold.
+ ArrayList<Call> calls = new ArrayList<>();
+ Call parentCall = createActiveCall();
+ final Call foregroundCall = mock(Call.class);
+ final Call heldCall = createHeldCall();
+ calls.add(parentCall);
+ calls.add(foregroundCall);
+ calls.add(heldCall);
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ when(foregroundCall.getState()).thenReturn(CallState.ACTIVE);
+ when(heldCall.getState()).thenReturn(CallState.ACTIVE);
+ when(foregroundCall.isIncoming()).thenReturn(false);
+ when(heldCall.isIncoming()).thenReturn(true);
+ when(foregroundCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+ when(heldCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0001")));
+ addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+ addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+ removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentCall.getConferenceLevelActiveCall()).thenReturn(foregroundCall);
+ when(parentCall.isConference()).thenReturn(true);
+ when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+ add(foregroundCall);
+ add(heldCall);
+ }});
+ //Add links from child calls to parent
+ when(foregroundCall.getParentCall()).thenReturn(parentCall);
+ when(heldCall.getParentCall()).thenReturn(parentCall);
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+
+ verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(CALL_STATE_ACTIVE), eq(0),
+ eq(false), eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown));
+ verify(mMockBluetoothHeadset).clccResponse(eq(2), eq(1), eq(CALL_STATE_HELD), eq(0),
+ eq(false), eq("555-0001"), eq(PhoneNumberUtils.TOA_Unknown));
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ }
+
+ public void testListCurrentCallsCdmaConference() throws Exception {
+ // Call is in a true CDMA conference
+ ArrayList<Call> calls = new ArrayList<>();
+ Call parentCall = createActiveCall();
+ final Call confCall1 = mock(Call.class);
+ final Call confCall2 = createHeldCall();
+ calls.add(parentCall);
+ calls.add(confCall1);
+ calls.add(confCall2);
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ when(confCall1.getState()).thenReturn(CallState.ACTIVE);
+ when(confCall2.getState()).thenReturn(CallState.ACTIVE);
+ when(confCall1.isIncoming()).thenReturn(false);
+ when(confCall2.isIncoming()).thenReturn(true);
+ when(confCall1.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+ when(confCall2.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0001")));
+ removeCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+ removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentCall.wasConferencePreviouslyMerged()).thenReturn(true);
+ when(parentCall.getConferenceLevelActiveCall()).thenReturn(confCall1);
+ when(parentCall.isConference()).thenReturn(true);
+ when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+ add(confCall1);
+ add(confCall2);
+ }});
+ //Add links from child calls to parent
+ when(confCall1.getParentCall()).thenReturn(parentCall);
+ when(confCall2.getParentCall()).thenReturn(parentCall);
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+
+ verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(CALL_STATE_ACTIVE), eq(0),
+ eq(true), eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown));
+ verify(mMockBluetoothHeadset).clccResponse(eq(2), eq(1), eq(CALL_STATE_ACTIVE), eq(0),
+ eq(true), eq("555-0001"), eq(PhoneNumberUtils.TOA_Unknown));
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ }
+
+ public void testWaitingCallClccResponse() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ // This test does not define a value for getForegroundCall(), so this ringing call will
+ // be treated as if it is a waiting call when listCurrentCalls() is invoked.
+ Call waitingCall = createRingingCall();
+ calls.add(waitingCall);
+ when(waitingCall.isIncoming()).thenReturn(true);
+ when(waitingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+ when(waitingCall.getState()).thenReturn(CallState.RINGING);
+ when(waitingCall.isConference()).thenReturn(false);
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_WAITING, 0, false,
+ "555-0000", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ verify(mMockBluetoothHeadset, times(2)).clccResponse(anyInt(),
+ anyInt(), anyInt(), anyInt(), anyBoolean(), anyString(), anyInt());
+ }
+
+ public void testNewCallClccResponse() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ Call newCall = createForegroundCall();
+ calls.add(newCall);
+ when(newCall.getState()).thenReturn(CallState.NEW);
+ when(newCall.isConference()).thenReturn(false);
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ verify(mMockBluetoothHeadset, times(1)).clccResponse(anyInt(),
+ anyInt(), anyInt(), anyInt(), anyBoolean(), anyString(), anyInt());
+ }
+
+ public void testRingingCallClccResponse() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ Call ringingCall = createForegroundCall();
+ calls.add(ringingCall);
+ when(ringingCall.getState()).thenReturn(CallState.RINGING);
+ when(ringingCall.isIncoming()).thenReturn(true);
+ when(ringingCall.isConference()).thenReturn(false);
+ when(ringingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_INCOMING, 0, false,
+ "555-0000", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ verify(mMockBluetoothHeadset, times(2)).clccResponse(anyInt(),
+ anyInt(), anyInt(), anyInt(), anyBoolean(), anyString(), anyInt());
+ }
+
+ public void testCallClccCache() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ Call ringingCall = createForegroundCall();
+ calls.add(ringingCall);
+ when(ringingCall.getState()).thenReturn(CallState.RINGING);
+ when(ringingCall.isIncoming()).thenReturn(true);
+ when(ringingCall.isConference()).thenReturn(false);
+ when(ringingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_INCOMING, 0, false,
+ "555-0000", PhoneNumberUtils.TOA_Unknown);
+
+ // Test Caching of old call indicies in clcc
+ when(ringingCall.getState()).thenReturn(CallState.ACTIVE);
+ Call newHoldingCall = createHeldCall();
+ calls.add(0, newHoldingCall);
+ when(newHoldingCall.getState()).thenReturn(CallState.ON_HOLD);
+ when(newHoldingCall.isIncoming()).thenReturn(true);
+ when(newHoldingCall.isConference()).thenReturn(false);
+ when(newHoldingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0001")));
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_ACTIVE, 0, false,
+ "555-0000", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset).clccResponse(2, 1, CALL_STATE_HELD, 0, false,
+ "555-0001", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset, times(2)).clccResponse(0, 0, 0, 0, false, null, 0);
+ }
+
+ public void testAlertingCallClccResponse() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ Call dialingCall = createForegroundCall();
+ calls.add(dialingCall);
+ when(dialingCall.getState()).thenReturn(CallState.DIALING);
+ when(dialingCall.isIncoming()).thenReturn(false);
+ when(dialingCall.isConference()).thenReturn(false);
+ when(dialingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(1, 0, CALL_STATE_ALERTING, 0, false,
+ "555-0000", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ verify(mMockBluetoothHeadset, times(2)).clccResponse(anyInt(),
+ anyInt(), anyInt(), anyInt(), anyBoolean(), anyString(), anyInt());
+ }
+
+ public void testHoldingCallClccResponse() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+ Call dialingCall = createForegroundCall();
+ calls.add(dialingCall);
+ when(dialingCall.getState()).thenReturn(CallState.DIALING);
+ when(dialingCall.isIncoming()).thenReturn(false);
+ when(dialingCall.isConference()).thenReturn(false);
+ when(dialingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0000")));
+ Call holdingCall = createHeldCall();
+ calls.add(holdingCall);
+ when(holdingCall.getState()).thenReturn(CallState.ON_HOLD);
+ when(holdingCall.isIncoming()).thenReturn(true);
+ when(holdingCall.isConference()).thenReturn(false);
+ when(holdingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+ Uri.parse("tel:555-0001")));
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+ verify(mMockBluetoothHeadset).clccResponse(1, 0, CALL_STATE_ALERTING, 0, false,
+ "555-0000", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset).clccResponse(2, 1, CALL_STATE_HELD, 0, false,
+ "555-0001", PhoneNumberUtils.TOA_Unknown);
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ verify(mMockBluetoothHeadset, times(3)).clccResponse(anyInt(),
+ anyInt(), anyInt(), anyInt(), anyBoolean(), anyString(), anyInt());
+ }
+
+ public void testListCurrentCallsImsConference() throws Exception {
+ ArrayList<Call> calls = new ArrayList<>();
+ Call parentCall = createActiveCall();
+ calls.add(parentCall);
+ addCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentCall.isConference()).thenReturn(true);
+ when(parentCall.getState()).thenReturn(CallState.ACTIVE);
+ when(parentCall.isIncoming()).thenReturn(true);
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+ mBluetoothPhoneService.mBinder.listCurrentCalls();
+
+ verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(1), eq(CALL_STATE_ACTIVE), eq(0),
+ eq(true), (String) isNull(), eq(-1));
+ verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+ }
+
+ public void testQueryPhoneState() throws Exception {
+ Call ringingCall = createRingingCall();
+ when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+
+ mBluetoothPhoneService.mBinder.queryPhoneState();
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+ eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown));
+ }
+
+ public void testCDMAConferenceQueryState() throws Exception {
+ Call parentConfCall = createActiveCall();
+ final Call confCall1 = mock(Call.class);
+ final Call confCall2 = mock(Call.class);
+ when(parentConfCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+ addCallCapability(parentConfCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+ removeCallCapability(parentConfCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentConfCall.wasConferencePreviouslyMerged()).thenReturn(true);
+ when(parentConfCall.isConference()).thenReturn(true);
+ when(parentConfCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+ add(confCall1);
+ add(confCall2);
+ }});
+
+ mBluetoothPhoneService.mBinder.queryPhoneState();
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+ }
+
+ public void testProcessChldTypeReleaseHeldRinging() throws Exception {
+ Call ringingCall = createRingingCall();
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_RELEASEHELD);
+
+ verify(mMockCallsManager).rejectCall(eq(ringingCall), eq(false), any(String.class));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldTypeReleaseHeldHold() throws Exception {
+ Call onHoldCall = createHeldCall();
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_RELEASEHELD);
+
+ verify(mMockCallsManager).disconnectCall(eq(onHoldCall));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldReleaseActiveRinging() throws Exception {
+ Call activeCall = createActiveCall();
+ Call ringingCall = createRingingCall();
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+ CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD);
+
+ verify(mMockCallsManager).disconnectCall(eq(activeCall));
+ verify(mMockCallsManager).answerCall(eq(ringingCall), any(int.class));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldReleaseActiveHold() throws Exception {
+ Call activeCall = createActiveCall();
+ Call heldCall = createHeldCall();
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+ CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD);
+
+ verify(mMockCallsManager).disconnectCall(eq(activeCall));
+ verify(mMockCallsManager).unholdCall(eq(heldCall));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldHoldActiveRinging() throws Exception {
+ Call ringingCall = createRingingCall();
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+ CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+ verify(mMockCallsManager).answerCall(eq(ringingCall), any(int.class));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldHoldActiveUnhold() throws Exception {
+ Call heldCall = createHeldCall();
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+ CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+ verify(mMockCallsManager).unholdCall(eq(heldCall));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldHoldActiveHold() throws Exception {
+ Call activeCall = createActiveCall();
+ addCallCapability(activeCall, Connection.CAPABILITY_HOLD);
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+ CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+ verify(mMockCallsManager).holdCall(eq(activeCall));
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldAddHeldToConfHolding() throws Exception {
+ Call activeCall = createActiveCall();
+ addCallCapability(activeCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_ADDHELDTOCONF);
+
+ verify(activeCall).mergeConference();
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldAddHeldToConf() throws Exception {
+ Call activeCall = createActiveCall();
+ removeCallCapability(activeCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+ Call conferenceableCall = mock(Call.class);
+ ArrayList<Call> conferenceableCalls = new ArrayList<>();
+ conferenceableCalls.add(conferenceableCall);
+ when(activeCall.getConferenceableCalls()).thenReturn(conferenceableCalls);
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_ADDHELDTOCONF);
+
+ verify(mMockCallsManager).conference(activeCall, conferenceableCall);
+ assertEquals(didProcess, true);
+ }
+
+ public void testProcessChldHoldActiveSwapConference() throws Exception {
+ // Create an active CDMA Call with a call on hold and simulate a swapConference().
+ Call parentCall = createActiveCall();
+ final Call foregroundCall = mock(Call.class);
+ final Call heldCall = createHeldCall();
+ addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+ removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentCall.isConference()).thenReturn(true);
+ when(parentCall.wasConferencePreviouslyMerged()).thenReturn(false);
+ when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+ add(foregroundCall);
+ add(heldCall);
+ }});
+
+ boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+ CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+ verify(parentCall).swapConference();
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE), eq(""),
+ eq(128));
+ assertEquals(didProcess, true);
+ }
+
+ // Testing the CallsManager Listener Functionality on Bluetooth
+ public void testOnCallAddedRinging() throws Exception {
+ Call ringingCall = createRingingCall();
+ when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-000"));
+
+ mBluetoothPhoneService.mCallsManagerListener.onCallAdded(ringingCall);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+ eq("555-000"), eq(PhoneNumberUtils.TOA_Unknown));
+
+ }
+
+ public void testOnCallAddedCdmaActiveHold() throws Exception {
+ // Call has been put into a CDMA "conference" with one call on hold.
+ Call parentCall = createActiveCall();
+ final Call foregroundCall = mock(Call.class);
+ final Call heldCall = createHeldCall();
+ addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+ removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ when(parentCall.isConference()).thenReturn(true);
+ when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+ add(foregroundCall);
+ add(heldCall);
+ }});
+
+ mBluetoothPhoneService.mCallsManagerListener.onCallAdded(parentCall);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+
+ }
+
+ public void testOnCallRemoved() throws Exception {
+ Call activeCall = createActiveCall();
+ mBluetoothPhoneService.mCallsManagerListener.onCallAdded(activeCall);
+ doReturn(null).when(mMockCallsManager).getActiveCall();
+ mBluetoothPhoneService.mCallsManagerListener.onCallRemoved(activeCall);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+ }
+
+ public void testOnCallStateChangedConnectingCall() throws Exception {
+ Call activeCall = mock(Call.class);
+ Call connectingCall = mock(Call.class);
+ when(connectingCall.getState()).thenReturn(CallState.CONNECTING);
+ ArrayList<Call> calls = new ArrayList<>();
+ calls.add(connectingCall);
+ calls.add(activeCall);
+ when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+ mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(activeCall,
+ CallState.ACTIVE, CallState.ON_HOLD);
+
+ verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+ anyString(), anyInt());
+ }
+
+ public void testOnCallStateChangedDialing() throws Exception {
+ Call activeCall = createActiveCall();
+
+ mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(activeCall,
+ CallState.CONNECTING, CallState.DIALING);
+
+ verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+ anyString(), anyInt());
+ }
+
+ public void testOnCallStateChangedAlerting() throws Exception {
+ Call outgoingCall = createOutgoingCall();
+
+ mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(outgoingCall,
+ CallState.NEW, CallState.DIALING);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(0, 0, CALL_STATE_DIALING, "", 128);
+ verify(mMockBluetoothHeadset).phoneStateChanged(0, 0, CALL_STATE_ALERTING, "", 128);
+ }
+
+ public void testOnCallStateChanged() throws Exception {
+ Call ringingCall = createRingingCall();
+ when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+ mBluetoothPhoneService.mCallsManagerListener.onCallAdded(ringingCall);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+ eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown));
+
+ //Switch to active
+ doReturn(null).when(mMockCallsManager).getRingingCall();
+ when(mMockCallsManager.getActiveCall()).thenReturn(ringingCall);
+
+ mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(ringingCall,
+ CallState.RINGING, CallState.ACTIVE);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+ }
+
+ public void testOnCallStateChangedGSMSwap() throws Exception {
+ Call heldCall = createHeldCall();
+ when(heldCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+ doReturn(2).when(mMockCallsManager).getNumHeldCalls();
+ mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(heldCall,
+ CallState.ACTIVE, CallState.ON_HOLD);
+
+ verify(mMockBluetoothHeadset, never()).phoneStateChanged(eq(0), eq(2), eq(CALL_STATE_HELD),
+ eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown));
+ }
+
+ public void testOnIsConferencedChanged() throws Exception {
+ // Start with two calls that are being merged into a CDMA conference call. The
+ // onIsConferencedChanged method will be called multiple times during the call. Make sure
+ // that the bluetooth phone state is updated properly.
+ Call parentCall = createActiveCall();
+ Call activeCall = mock(Call.class);
+ Call heldCall = createHeldCall();
+ when(activeCall.getParentCall()).thenReturn(parentCall);
+ when(heldCall.getParentCall()).thenReturn(parentCall);
+ ArrayList<Call> calls = new ArrayList<>();
+ calls.add(activeCall);
+ when(parentCall.getChildCalls()).thenReturn(calls);
+ when(parentCall.isConference()).thenReturn(true);
+ removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+ addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+ when(parentCall.wasConferencePreviouslyMerged()).thenReturn(false);
+
+ // Be sure that onIsConferencedChanged rejects spurious changes during set up of
+ // CDMA "conference"
+ mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(activeCall);
+ verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+ anyString(), anyInt());
+ mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(heldCall);
+ verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+ anyString(), anyInt());
+ mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+ verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+ anyString(), anyInt());
+
+ calls.add(heldCall);
+ mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+ eq(""), eq(128));
+ }
+
+ public void testBluetoothAdapterReceiver() throws Exception {
+ Call ringingCall = createRingingCall();
+ when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+
+ Intent intent = new Intent();
+ intent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON);
+ mBluetoothPhoneService.mBluetoothAdapterReceiver.onReceive(mContext, intent);
+
+ verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+ eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown));
+ }
+
+ private void addCallCapability(Call call, int capability) {
+ when(call.can(capability)).thenReturn(true);
+ }
+
+ private void removeCallCapability(Call call, int capability) {
+ when(call.can(capability)).thenReturn(false);
+ }
+
+ private Call createActiveCall() {
+ Call call = mock(Call.class);
+ when(mMockCallsManager.getActiveCall()).thenReturn(call);
+ return call;
+ }
+
+ private Call createRingingCall() {
+ Call call = mock(Call.class);
+ when(mMockCallsManager.getRingingCall()).thenReturn(call);
+ return call;
+ }
+
+ private Call createHeldCall() {
+ Call call = mock(Call.class);
+ when(mMockCallsManager.getHeldCall()).thenReturn(call);
+ return call;
+ }
+
+ private Call createOutgoingCall() {
+ Call call = mock(Call.class);
+ when(mMockCallsManager.getOutgoingCall()).thenReturn(call);
+ return call;
+ }
+
+ private Call createForegroundCall() {
+ Call call = mock(Call.class);
+ when(mMockCallsManager.getForegroundCall()).thenReturn(call);
+ return call;
+ }
+
+ private static ComponentName makeQuickConnectionServiceComponentName() {
+ return new ComponentName("com.android.server.telecom.tests",
+ "com.android.server.telecom.tests.MockConnectionService");
+ }
+
+ private static PhoneAccountHandle makeQuickAccountHandle(String id) {
+ return new PhoneAccountHandle(makeQuickConnectionServiceComponentName(), id,
+ Binder.getCallingUserHandle());
+ }
+
+ private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
+ return new PhoneAccount.Builder(makeQuickAccountHandle(id), "label" + idx);
+ }
+
+ private PhoneAccount makeQuickAccount(String id, int idx) {
+ return makeQuickAccountBuilder(id, idx)
+ .setAddress(Uri.parse(TEST_ACCOUNT_ADDRESS + idx))
+ .setSubscriptionAddress(Uri.parse("tel:555-000" + idx))
+ .setCapabilities(idx)
+ .setIcon(Icon.createWithResource(
+ "com.android.server.telecom.tests", R.drawable.stat_sys_phone_call))
+ .setShortDescription("desc" + idx)
+ .setIsEnabled(true)
+ .build();
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
new file mode 100644
index 0000000..4c32dd7
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context;
+import android.media.AudioManager;
+import android.telecom.CallAudioState;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallAudioModeStateMachine;
+
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+public class CallAudioModeStateMachineTest extends StateMachineTestBase<CallAudioModeStateMachine> {
+ private static class ModeTestParameters extends TestParameters {
+ public String name;
+ public int initialAudioState; // One of the explicit switch focus constants in CAMSM
+ public int messageType; // Any of the commands from the state machine
+ public CallAudioModeStateMachine.MessageArgs externalState;
+ public String expectedFinalStateName;
+ public int expectedFocus; // one of the FOCUS_* constants below
+ public int expectedMode; // NO_CHANGE, or an AudioManager.MODE_* constant
+ public int expectedRingingInteraction; // NO_CHANGE, ON, or OFF
+ public int expectedCallWaitingInteraction; // NO_CHANGE, ON, or OFF
+
+ public ModeTestParameters(String name, int initialAudioState, int messageType,
+ CallAudioModeStateMachine.MessageArgs externalState, String
+ expectedFinalStateName, int expectedFocus, int expectedMode, int
+ expectedRingingInteraction, int expectedCallWaitingInteraction) {
+ this.name = name;
+ this.initialAudioState = initialAudioState;
+ this.messageType = messageType;
+ this.externalState = externalState;
+ this.expectedFinalStateName = expectedFinalStateName;
+ this.expectedFocus = expectedFocus;
+ this.expectedMode = expectedMode;
+ this.expectedRingingInteraction = expectedRingingInteraction;
+ this.expectedCallWaitingInteraction = expectedCallWaitingInteraction;
+ }
+
+ @Override
+ public String toString() {
+ return "ModeTestParameters{" +
+ "name='" + name + '\'' +
+ ", initialAudioState=" + initialAudioState +
+ ", messageType=" + messageType +
+ ", externalState=" + externalState +
+ ", expectedFinalStateName='" + expectedFinalStateName + '\'' +
+ ", expectedFocus=" + expectedFocus +
+ ", expectedMode=" + expectedMode +
+ ", expectedRingingInteraction=" + expectedRingingInteraction +
+ ", expectedCallWaitingInteraction=" + expectedCallWaitingInteraction +
+ '}';
+ }
+ }
+
+ private static final int FOCUS_NO_CHANGE = 0;
+ private static final int FOCUS_RING = 1;
+ private static final int FOCUS_VOICE = 2;
+ private static final int FOCUS_OFF = 3;
+
+ private static final int NO_CHANGE = -1;
+ private static final int ON = 0;
+ private static final int OFF = 1;
+
+ @Mock private AudioManager mAudioManager;
+ @Mock private CallAudioManager mCallAudioManager;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ }
+
+ @LargeTest
+ public void testTransitions() throws Throwable {
+ List<ModeTestParameters> testCases = generateTestCases();
+ parametrizedTestStateMachine(testCases);
+ }
+
+ private List<ModeTestParameters> generateTestCases() {
+ List<ModeTestParameters> result = new ArrayList<>();
+ result.add(new ModeTestParameters(
+ "New active/dialing call with no other calls when unfocused",
+ CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "New active/dialing voip call with no other calls when unfocused",
+ CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ true, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.COMMS_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_COMMUNICATION, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "New ringing call with no other calls when unfocused",
+ CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_RINGING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ true, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.RING_STATE_NAME, // expectedFinalStateName
+ FOCUS_RING, // expectedFocus
+ AudioManager.MODE_RINGTONE, // expectedMode
+ ON, // expectedRingingInteraction
+ OFF // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "New ringing call coming in on top of active/dialing call",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_RINGING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ true, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ ON // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Ringing call becomes active, part 1",
+ CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ OFF, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Ringing call becomes active, part 2",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Active call disconnects, but tone is playing",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ true, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.TONE_HOLD_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Tone stops playing, with no active calls",
+ CallAudioModeStateMachine.ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.TONE_STOPPED_PLAYING, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.UNFOCUSED_STATE_NAME, // expectedFinalStateName
+ FOCUS_OFF, // expectedFocus
+ AudioManager.MODE_NORMAL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Ringing call disconnects",
+ CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.UNFOCUSED_STATE_NAME, // expectedFinalStateName
+ FOCUS_OFF, // expectedFocus
+ AudioManager.MODE_NORMAL, // expectedMode
+ OFF, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call-waiting call disconnects",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ true, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ OFF // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call is placed on hold - 1",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ true, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.TONE_HOLD_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call is placed on hold - 2",
+ CallAudioModeStateMachine.ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_HOLDING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ true, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.TONE_HOLD_STATE_NAME, // expectedFinalStateName
+ FOCUS_NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call is taken off hold - 1",
+ CallAudioModeStateMachine.ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call is taken off hold - 2",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Active call disconnects while there's a call-waiting call",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ true, // hasRingingCalls
+ false, // hasHoldingCalls
+ true, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.RING_STATE_NAME, // expectedFinalStateName
+ FOCUS_RING, // expectedFocus
+ AudioManager.MODE_RINGTONE, // expectedMode
+ ON, // expectedRingingInteraction
+ OFF // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "New dialing call when there's a call on hold",
+ CallAudioModeStateMachine.ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ true, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Ringing call disconnects with a holding call in the background",
+ CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ false, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ true, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.TONE_HOLD_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_NORMAL, // expectedMode -- we're expecting this because
+ // mMostRecentMode hasn't been set properly.
+ OFF, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Foreground call transitions from sim to voip",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ true, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.COMMS_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_COMMUNICATION, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Foreground call transitions from voip to sim",
+ CallAudioModeStateMachine.ENTER_COMMS_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ false, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_VOICE, // expectedFocus
+ AudioManager.MODE_IN_CALL, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ NO_CHANGE // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call-waiting hangs up before being answered, with another sim call in " +
+ "foreground",
+ CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ true, // isTonePlaying
+ false, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.CALL_STATE_NAME, // expectedFinalStateName
+ FOCUS_NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ OFF // expectedCallWaitingInteraction
+ ));
+
+ result.add(new ModeTestParameters(
+ "Call-waiting hangs up before being answered, with another voip call in " +
+ "foreground",
+ CallAudioModeStateMachine.ENTER_COMMS_FOCUS_FOR_TESTING, // initialAudioState
+ CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, // messageType
+ new CallAudioModeStateMachine.MessageArgs(
+ true, // hasActiveOrDialingCalls
+ false, // hasRingingCalls
+ false, // hasHoldingCalls
+ true, // isTonePlaying
+ true, // foregroundCallIsVoip
+ null // session
+ ),
+ CallAudioModeStateMachine.COMMS_STATE_NAME, // expectedFinalStateName
+ FOCUS_NO_CHANGE, // expectedFocus
+ NO_CHANGE, // expectedMode
+ NO_CHANGE, // expectedRingingInteraction
+ OFF // expectedCallWaitingInteraction
+ ));
+
+ return result;
+ }
+
+ @Override
+ protected void runParametrizedTestCase(TestParameters _params) {
+ ModeTestParameters params = (ModeTestParameters) _params;
+ CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mAudioManager);
+ sm.setCallAudioManager(mCallAudioManager);
+ sm.sendMessage(params.initialAudioState);
+ waitForStateMachineActionCompletion(sm, CallAudioModeStateMachine.RUN_RUNNABLE);
+
+ resetMocks();
+
+ sm.sendMessage(params.messageType, params.externalState);
+ waitForStateMachineActionCompletion(sm, CallAudioModeStateMachine.RUN_RUNNABLE);
+
+ assertEquals(params.expectedFinalStateName, sm.getCurrentStateName());
+
+ switch (params.expectedFocus) {
+ case FOCUS_NO_CHANGE:
+ verify(mAudioManager, never()).requestAudioFocusForCall(anyInt(), anyInt());
+ break;
+ case FOCUS_OFF:
+ verify(mAudioManager).abandonAudioFocusForCall();
+ break;
+ case FOCUS_RING:
+ verify(mAudioManager).requestAudioFocusForCall(
+ eq(AudioManager.STREAM_RING), anyInt());
+ break;
+ case FOCUS_VOICE:
+ verify(mAudioManager).requestAudioFocusForCall(
+ eq(AudioManager.STREAM_VOICE_CALL), anyInt());
+ break;
+ }
+
+ if (params.expectedMode != NO_CHANGE) {
+ verify(mAudioManager).setMode(eq(params.expectedMode));
+ } else {
+ verify(mAudioManager, never()).setMode(anyInt());
+ }
+
+ switch (params.expectedRingingInteraction) {
+ case NO_CHANGE:
+ verify(mCallAudioManager, never()).startRinging();
+ verify(mCallAudioManager, never()).stopRinging();
+ break;
+ case ON:
+ verify(mCallAudioManager).startRinging();
+ break;
+ case OFF:
+ verify(mCallAudioManager).stopRinging();
+ break;
+ }
+
+ switch (params.expectedCallWaitingInteraction) {
+ case NO_CHANGE:
+ verify(mCallAudioManager, never()).startCallWaiting();
+ verify(mCallAudioManager, never()).stopCallWaiting();
+ break;
+ case ON:
+ verify(mCallAudioManager).startCallWaiting();
+ break;
+ case OFF:
+ verify(mCallAudioManager).stopCallWaiting();
+ break;
+ }
+ }
+
+ private void resetMocks() {
+ reset(mCallAudioManager, mAudioManager);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
new file mode 100644
index 0000000..776bd8c
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -0,0 +1,751 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.telecom.CallAudioState;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.BluetoothManager;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioModeStateMachine;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.WiredHeadsetManager;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+public class CallAudioRouteStateMachineTest
+ extends StateMachineTestBase<CallAudioRouteStateMachine> {
+ private static final int NONE = 0;
+ private static final int ON = 1;
+ private static final int OFF = 2;
+
+ static class RoutingTestParameters extends TestParameters {
+ public String name;
+ public int initialRoute;
+ public int availableRoutes; // may excl. speakerphone, because that's always available
+ public int speakerInteraction; // one of NONE, ON, or OFF
+ public int bluetoothInteraction; // one of NONE, ON, or OFF
+ public int action;
+ public int expectedRoute;
+ public int expectedAvailableRoutes; // also may exclude the speakerphone.
+ public boolean doesDeviceSupportEarpiece; // set to false in the case of Wear devices
+ public boolean shouldRunWithFocus;
+
+ public RoutingTestParameters(String name, int initialRoute, int availableRoutes, int
+ speakerInteraction, int bluetoothInteraction, int action, int expectedRoute, int
+ expectedAvailableRoutes, boolean doesDeviceSupportEarpiece,
+ boolean shouldRunWithFocus) {
+ this.name = name;
+ this.initialRoute = initialRoute;
+ this.availableRoutes = availableRoutes;
+ this.speakerInteraction = speakerInteraction;
+ this.bluetoothInteraction = bluetoothInteraction;
+ this.action = action;
+ this.expectedRoute = expectedRoute;
+ this.expectedAvailableRoutes = expectedAvailableRoutes;
+ this.doesDeviceSupportEarpiece = doesDeviceSupportEarpiece;
+ this.shouldRunWithFocus = shouldRunWithFocus;
+ }
+
+ @Override
+ public String toString() {
+ return "RoutingTestParameters{" +
+ "name='" + name + '\'' +
+ ", initialRoute=" + initialRoute +
+ ", availableRoutes=" + availableRoutes +
+ ", speakerInteraction=" + speakerInteraction +
+ ", bluetoothInteraction=" + bluetoothInteraction +
+ ", action=" + action +
+ ", expectedRoute=" + expectedRoute +
+ ", expectedAvailableRoutes=" + expectedAvailableRoutes +
+ ", doesDeviceSupportEarpiece=" + doesDeviceSupportEarpiece +
+ ", shouldRunWithFocus=" + shouldRunWithFocus +
+ '}';
+ }
+ }
+
+ @Mock CallsManager mockCallsManager;
+ @Mock BluetoothManager mockBluetoothManager;
+ @Mock IAudioService mockAudioService;
+ @Mock ConnectionServiceWrapper mockConnectionServiceWrapper;
+ @Mock WiredHeadsetManager mockWiredHeadsetManager;
+ @Mock StatusBarNotifier mockStatusBarNotifier;
+ @Mock Call fakeCall;
+
+ private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+ private static final int TEST_TIMEOUT = 500;
+ private AudioManager mockAudioManager;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+ mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
+ @Override
+ public IAudioService getAudioService() {
+ return mockAudioService;
+ }
+ };
+
+ when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
+ when(fakeCall.isAlive()).thenReturn(true);
+ doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
+ any(CallAudioState.class));
+ }
+
+ @LargeTest
+ public void testStateMachineTransitionsWithFocus() throws Throwable {
+ List<RoutingTestParameters> paramList = generateTransitionTests(true);
+ parametrizedTestStateMachine(paramList);
+ }
+
+ @LargeTest
+ public void testStateMachineTransitionsWithoutFocus() throws Throwable {
+ List<RoutingTestParameters> paramList = generateTransitionTests(false);
+ parametrizedTestStateMachine(paramList);
+ }
+
+ @MediumTest
+ public void testSpeakerPersistence() {
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ true);
+
+ when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(true);
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn((Boolean) args[0]);
+ return null;
+ }
+ }).when(mockAudioManager).setSpeakerphoneOn(any(Boolean.class));
+ CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ stateMachine.initialize(initState);
+
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.HAS_FOCUS);
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+ CallAudioState expectedMiddleState = new CallAudioState(false,
+ CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+ verifyNewSystemCallAudioState(initState, expectedMiddleState);
+ resetMocks();
+
+ stateMachine.sendMessageWithSessionInfo(
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+ verifyNewSystemCallAudioState(expectedMiddleState, initState);
+ }
+
+ @SmallTest
+ public void testInitializationWithEarpieceNoHeadsetNoBluetooth() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+ initializationTestHelper(expectedState, true);
+ }
+
+ @SmallTest
+ public void testInitializationWithEarpieceAndHeadsetNoBluetooth() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+ initializationTestHelper(expectedState, true);
+ }
+
+ @SmallTest
+ public void testInitializationWithEarpieceAndHeadsetAndBluetooth() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ initializationTestHelper(expectedState, true);
+ }
+
+ @SmallTest
+ public void testInitializationWithEarpieceAndBluetoothNoHeadset() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ initializationTestHelper(expectedState, true);
+ }
+
+ @SmallTest
+ public void testInitializationWithNoEarpieceNoHeadsetNoBluetooth() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+ CallAudioState.ROUTE_SPEAKER);
+ initializationTestHelper(expectedState, false);
+ }
+
+ @SmallTest
+ public void testInitializationWithHeadsetNoBluetoothNoEarpiece() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+ initializationTestHelper(expectedState, false);
+ }
+
+ @SmallTest
+ public void testInitializationWithHeadsetAndBluetoothNoEarpiece() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+ | CallAudioState.ROUTE_BLUETOOTH);
+ initializationTestHelper(expectedState, false);
+ }
+
+ @SmallTest
+ public void testInitializationWithBluetoothNoHeadsetNoEarpiece() {
+ CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+ CallAudioState.ROUTE_SPEAKER | CallAudioState.ROUTE_BLUETOOTH);
+ initializationTestHelper(expectedState, false);
+ }
+
+ private void initializationTestHelper(CallAudioState expectedState,
+ boolean doesDeviceSupportEarpiece) {
+ when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(
+ (expectedState.getSupportedRouteMask() & CallAudioState.ROUTE_WIRED_HEADSET) != 0);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+ (expectedState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH) != 0);
+
+ CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ doesDeviceSupportEarpiece);
+ stateMachine.initialize();
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ private List<RoutingTestParameters> generateTransitionTests(boolean shouldRunWithFocus) {
+ List<RoutingTestParameters> params = new ArrayList<>();
+ params.add(new RoutingTestParameters(
+ "Connect headset during earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Connect headset during bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Connect headset during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ OFF, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect headset during headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect headset during headset with bluetooth available", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect headset during bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect headset during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect headset during speakerphone with bluetooth available", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Connect bluetooth during earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Connect bluetooth during wired headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvai
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Connect bluetooth during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ OFF, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect bluetooth during bluetooth without headset in", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect bluetooth during bluetooth with headset in", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect bluetooth during speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect bluetooth during earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to speakerphone from earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ ON, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to speakerphone from headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ ON, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to speakerphone from bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ ON, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to earpiece from bluetooth", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to earpiece from speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE, // availableRoutes
+ OFF, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+ CallAudioState.ROUTE_EARPIECE, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to bluetooth from speakerphone", // name
+ CallAudioState.ROUTE_SPEAKER, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ OFF, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to bluetooth from earpiece", // name
+ CallAudioState.ROUTE_EARPIECE, // initialRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch to bluetooth from wired headset", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+ NONE, // speakerInteraction
+ ON, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+ CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+ CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
+ true, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Switch from bluetooth to wired/earpiece when neither are available", // name
+ CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+ CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+ ON, // speakerInteraction
+ OFF, // bluetoothInteraction
+ CallAudioRouteStateMachine.SWITCH_BASELINE_ROUTE, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_BLUETOOTH, // expectedAvailableRoutes
+ false, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ params.add(new RoutingTestParameters(
+ "Disconnect wired headset when device does not support earpiece", // name
+ CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+ CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+ ON, // speakerInteraction
+ NONE, // bluetoothInteraction
+ CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+ CallAudioState.ROUTE_SPEAKER, // expectedRoute
+ CallAudioState.ROUTE_SPEAKER, // expectedAvailableRoutes
+ false, // doesDeviceSupportEarpiece
+ shouldRunWithFocus
+ ));
+
+ return params;
+ }
+
+ @Override
+ protected void runParametrizedTestCase(TestParameters _params) throws Throwable {
+ RoutingTestParameters params = (RoutingTestParameters) _params;
+ if (params.shouldRunWithFocus) {
+ runParametrizedTestCaseWithFocus(params);
+ } else {
+ runParametrizedTestCaseWithoutFocus(params);
+ }
+ }
+
+ private void runParametrizedTestCaseWithFocus(final RoutingTestParameters params)
+ throws Throwable {
+ resetMocks();
+
+ // Construct a fresh state machine on every case
+ final CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ params.doesDeviceSupportEarpiece);
+
+ // Set up bluetooth and speakerphone state
+ when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(
+ params.initialRoute == CallAudioState.ROUTE_BLUETOOTH);
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+ (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
+ || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
+ params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+
+ // Set the initial CallAudioState object
+ final CallAudioState initState = new CallAudioState(false,
+ params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
+ stateMachine.initialize(initState);
+ // Make the state machine have focus so that we actually do something
+ stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+ CallAudioRouteStateMachine.HAS_FOCUS);
+ stateMachine.sendMessageWithSessionInfo(params.action);
+
+ waitForStateMachineActionCompletion(stateMachine, CallAudioRouteStateMachine.RUN_RUNNABLE);
+
+ stateMachine.quitStateMachine();
+
+ // Verify interactions with the speakerphone and bluetooth systems
+ switch (params.bluetoothInteraction) {
+ case NONE:
+ verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+ verify(mockBluetoothManager, never()).connectBluetoothAudio();
+ break;
+ case ON:
+ verify(mockBluetoothManager).connectBluetoothAudio();
+
+ verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+ break;
+ case OFF:
+ verify(mockBluetoothManager, never()).connectBluetoothAudio();
+ verify(mockBluetoothManager).disconnectBluetoothAudio();
+ }
+
+ switch (params.speakerInteraction) {
+ case NONE:
+ verify(mockAudioManager, never()).setSpeakerphoneOn(any(Boolean.class));
+ break;
+ case ON: // fall through
+ case OFF:
+ verify(mockAudioManager).setSpeakerphoneOn(params.speakerInteraction == ON);
+ }
+
+ // Verify the end state
+ CallAudioState expectedState = new CallAudioState(false, params.expectedRoute,
+ params.expectedAvailableRoutes | CallAudioState.ROUTE_SPEAKER);
+ verifyNewSystemCallAudioState(initState, expectedState);
+ }
+
+ private void runParametrizedTestCaseWithoutFocus(final RoutingTestParameters params)
+ throws Throwable {
+ resetMocks();
+
+ // Construct a fresh state machine on every case
+ final CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+ mContext,
+ mockCallsManager,
+ mockBluetoothManager,
+ mockWiredHeadsetManager,
+ mockStatusBarNotifier,
+ mAudioServiceFactory,
+ params.doesDeviceSupportEarpiece);
+
+ // Set up bluetooth and speakerphone state
+ when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+ (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
+ || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
+ when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
+ params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+
+ // Set the initial CallAudioState object
+ CallAudioState initState = new CallAudioState(false,
+ params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
+ stateMachine.initialize(initState);
+ // Omit the focus-getting statement
+ stateMachine.sendMessageWithSessionInfo(params.action);
+
+ waitForStateMachineActionCompletion(stateMachine, CallAudioModeStateMachine.RUN_RUNNABLE);
+
+ stateMachine.quitStateMachine();
+
+ // Verify that no substantive interactions have taken place with the
+ // rest of the system
+ verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+ verify(mockBluetoothManager, never()).connectBluetoothAudio();
+ verify(mockAudioManager, never()).setSpeakerphoneOn(any(Boolean.class));
+ verify(mockCallsManager, never()).onCallAudioStateChanged(any(CallAudioState.class),
+ any(CallAudioState.class));
+ verify(mockConnectionServiceWrapper, never()).onCallAudioStateChanged(
+ any(Call.class), any(CallAudioState.class));
+
+ // Verify the end state
+ CallAudioState expectedState = new CallAudioState(false, params.expectedRoute,
+ params.expectedAvailableRoutes | CallAudioState.ROUTE_SPEAKER);
+ assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+ }
+
+ private void verifyNewSystemCallAudioState(CallAudioState expectedOldState,
+ CallAudioState expectedNewState) {
+ ArgumentCaptor<CallAudioState> oldStateCaptor = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ ArgumentCaptor<CallAudioState> newStateCaptor1 = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ ArgumentCaptor<CallAudioState> newStateCaptor2 = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ verify(mockCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+ oldStateCaptor.capture(), newStateCaptor1.capture());
+ verify(mockConnectionServiceWrapper, timeout(TEST_TIMEOUT).atLeastOnce())
+ .onCallAudioStateChanged(same(fakeCall), newStateCaptor2.capture());
+
+ assertTrue(oldStateCaptor.getValue().equals(expectedOldState));
+ assertTrue(newStateCaptor1.getValue().equals(expectedNewState));
+ assertTrue(newStateCaptor2.getValue().equals(expectedNewState));
+ }
+
+ private void resetMocks() {
+ reset(mockAudioManager, mockBluetoothManager, mockCallsManager,
+ mockConnectionServiceWrapper);
+ when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+ doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
+ any(CallAudioState.class));
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
new file mode 100644
index 0000000..7f5297a
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.pm.UserInfo;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallLogManager;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelephonyUtil;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Arrays;
+
+public class CallLogManagerTest extends TelecomTestCase {
+
+ private CallLogManager mCallLogManager;
+ private IContentProvider mContentProvider;
+ private PhoneAccountHandle mDefaultAccountHandle;
+ private PhoneAccountHandle mOtherUserAccountHandle;
+ private PhoneAccountHandle mManagedProfileAccountHandle;
+
+ private static final Uri TEL_PHONEHANDLE = Uri.parse("tel:5555551234");
+
+ private static final PhoneAccountHandle EMERGENCY_ACCT_HANDLE = TelephonyUtil
+ .getDefaultEmergencyPhoneAccount()
+ .getAccountHandle();
+
+ private static final int NO_VIDEO_STATE = VideoProfile.STATE_AUDIO_ONLY;
+ private static final int BIDIRECTIONAL_VIDEO_STATE = VideoProfile.STATE_BIDIRECTIONAL;
+ private static final String POST_DIAL_STRING = ";12345";
+ private static final String TEST_PHONE_ACCOUNT_ID= "testPhoneAccountId";
+
+ private static final int TEST_TIMEOUT_MILLIS = 200;
+ private static final int CURRENT_USER_ID = 0;
+ private static final int OTHER_USER_ID = 10;
+ private static final int MANAGED_USER_ID = 11;
+
+ @Mock
+ PhoneAccountRegistrar mMockPhoneAccountRegistrar;
+
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mCallLogManager = new CallLogManager(mContext, mMockPhoneAccountRegistrar);
+ mContentProvider = mContext.getContentResolver().acquireProvider("test");
+ mDefaultAccountHandle = new PhoneAccountHandle(
+ new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
+ TEST_PHONE_ACCOUNT_ID,
+ UserHandle.of(CURRENT_USER_ID)
+ );
+
+ mOtherUserAccountHandle = new PhoneAccountHandle(
+ new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
+ TEST_PHONE_ACCOUNT_ID,
+ UserHandle.of(OTHER_USER_ID)
+ );
+
+ mManagedProfileAccountHandle = new PhoneAccountHandle(
+ new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
+ TEST_PHONE_ACCOUNT_ID,
+ UserHandle.of(MANAGED_USER_ID)
+ );
+
+ UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ UserInfo userInfo = new UserInfo(CURRENT_USER_ID, "test", 0);
+ UserInfo otherUserInfo = new UserInfo(OTHER_USER_ID, "test2", 0);
+ UserInfo managedProfileUserInfo = new UserInfo(OTHER_USER_ID, "test3",
+ UserInfo.FLAG_MANAGED_PROFILE);
+
+ doAnswer(new Answer<Uri>() {
+ @Override
+ public Uri answer(InvocationOnMock invocation) throws Throwable {
+ return (Uri) invocation.getArguments()[1];
+ }
+ }).when(mContentProvider).insert(anyString(), any(Uri.class), any(ContentValues.class));
+
+ when(userManager.isUserRunning(any(UserHandle.class))).thenReturn(true);
+ when(userManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true);
+ when(userManager.hasUserRestriction(any(String.class), any(UserHandle.class)))
+ .thenReturn(false);
+ when(userManager.getUsers(any(Boolean.class)))
+ .thenReturn(Arrays.asList(userInfo, otherUserInfo, managedProfileUserInfo));
+ when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo);
+ when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo);
+ when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo);
+ }
+
+ @MediumTest
+ public void testDontLogCancelledCall() {
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.CANCELED,
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits,
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.DIALING, CallState.DISCONNECTED);
+ verifyNoInsertion();
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.DIALING, CallState.ABORTED);
+ verifyNoInsertion();
+ }
+
+ @MediumTest
+ public void testDontLogChoosingAccountCall() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.SELECT_PHONE_ACCOUNT,
+ CallState.DISCONNECTED);
+ verifyNoInsertion();
+ }
+
+ @MediumTest
+ public void testDontLogCallsFromEmergencyAccount() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(EMERGENCY_ACCT_HANDLE, 0));
+ mComponentContextFixture.putBooleanResource(R.bool.allow_emergency_numbers_in_call_log,
+ false);
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ EMERGENCY_ACCT_HANDLE, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits,
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ verifyNoInsertion();
+ }
+
+ @MediumTest
+ public void testLogCallDirectionOutgoing() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
+ public void testLogCallDirectionIncoming() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeIncomingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ null
+ );
+ mCallLogManager.onCallStateChanged(fakeIncomingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
+ }
+
+ @MediumTest
+ public void testLogCallDirectionMissed() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeMissedCall = makeFakeCall(
+ DisconnectCause.MISSED, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ null
+ );
+
+ mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.MISSED_TYPE));
+ }
+
+ @MediumTest
+ public void testCreationTimeAndAge() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ long currentTime = System.currentTimeMillis();
+ long duration = 1000L;
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ currentTime, // creationTimeMillis
+ duration, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsLong(CallLog.Calls.DATE),
+ Long.valueOf(currentTime));
+ assertEquals(insertedValues.getAsLong(CallLog.Calls.DURATION),
+ Long.valueOf(duration / 1000));
+ }
+
+ @MediumTest
+ public void testLogPhoneAccountId() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsString(CallLog.Calls.PHONE_ACCOUNT_ID),
+ TEST_PHONE_ACCOUNT_ID);
+ }
+
+ @MediumTest
+ public void testLogCorrectPhoneNumber() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsString(CallLog.Calls.NUMBER),
+ TEL_PHONEHANDLE.getSchemeSpecificPart());
+ assertEquals(insertedValues.getAsString(CallLog.Calls.POST_DIAL_DIGITS), POST_DIAL_STRING);
+ }
+
+ @MediumTest
+ public void testLogCallVideoFeatures() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeVideoCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ BIDIRECTIONAL_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeVideoCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertTrue((insertedValues.getAsInteger(CallLog.Calls.FEATURES)
+ & CallLog.Calls.FEATURES_VIDEO) == CallLog.Calls.FEATURES_VIDEO);
+ }
+
+ @MediumTest
+ public void testLogCallDirectionOutgoingWithMultiUserCapability() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mOtherUserAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call placed through a phone account with multi user capability is inserted to
+ // all users except managed profile.
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ insertedValues = verifyInsertionWithCapture(OTHER_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ verifyNoInsertionInUser(MANAGED_USER_ID);
+ }
+
+ @MediumTest
+ public void testLogCallDirectionIncomingWithMultiUserCapability() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mOtherUserAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeIncomingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ null
+ );
+ mCallLogManager.onCallStateChanged(fakeIncomingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Incoming call using a phone account with multi user capability is inserted to all users
+ // except managed profile.
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
+ insertedValues = verifyInsertionWithCapture(OTHER_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
+ verifyNoInsertionInUser(MANAGED_USER_ID);
+ }
+
+ @MediumTest
+ public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromManagedProfile() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle,
+ PhoneAccount.CAPABILITY_MULTI_USER));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mManagedProfileAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(MANAGED_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call placed through work dialer should be inserted to managed profile only.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
+ public void testLogCallDirectionOutgoingFromManagedProfile() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle, 0));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ false, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mManagedProfileAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(MANAGED_USER_ID)
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Outgoing call using phone account in managed profile should be inserted to managed
+ // profile only.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+ }
+
+ @MediumTest
+ public void testLogCallDirectionIngoingFromManagedProfile() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle, 0));
+ Call fakeOutgoingCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mManagedProfileAccountHandle, // phoneAccountHandle
+ NO_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ null
+ );
+ mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+ CallState.DISCONNECTED);
+
+ // Incoming call using phone account in managed profile should be inserted to managed
+ // profile only.
+ verifyNoInsertionInUser(CURRENT_USER_ID);
+ verifyNoInsertionInUser(OTHER_USER_ID);
+ ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
+ assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+ Integer.valueOf(Calls.INCOMING_TYPE));
+ }
+
+ /**
+ * Ensure call data usage is persisted to the call log when present in the call.
+ */
+ @MediumTest
+ public void testLogCallDataUsageSet() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeVideoCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ BIDIRECTIONAL_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID), // initiatingUser
+ 1000 // callDataUsage
+ );
+ mCallLogManager.onCallStateChanged(fakeVideoCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertEquals(Long.valueOf(1000), insertedValues.getAsLong(CallLog.Calls.DATA_USAGE));
+ }
+
+ /**
+ * Ensures call data usage is null in the call log when not set on the call.
+ */
+ @MediumTest
+ public void testLogCallDataUsageNotSet() {
+ when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+ .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+ Call fakeVideoCall = makeFakeCall(
+ DisconnectCause.OTHER, // disconnectCauseCode
+ false, // isConference
+ true, // isIncoming
+ 1L, // creationTimeMillis
+ 1000L, // ageMillis
+ TEL_PHONEHANDLE, // callHandle
+ mDefaultAccountHandle, // phoneAccountHandle
+ BIDIRECTIONAL_VIDEO_STATE, // callVideoState
+ POST_DIAL_STRING, // postDialDigits
+ UserHandle.of(CURRENT_USER_ID), // initiatingUser
+ Call.DATA_USAGE_NOT_SET // callDataUsage
+ );
+ mCallLogManager.onCallStateChanged(fakeVideoCall, CallState.ACTIVE, CallState.DISCONNECTED);
+ ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+ assertNull(insertedValues.getAsLong(CallLog.Calls.DATA_USAGE));
+ }
+
+ private void verifyNoInsertion() {
+ try {
+ verify(mContentProvider, timeout(TEST_TIMEOUT_MILLIS).never()).insert(any(String.class),
+ any(Uri.class), any(ContentValues.class));
+ } catch (android.os.RemoteException e) {
+ fail("Remote exception occurred during test execution");
+ }
+ }
+
+
+ private void verifyNoInsertionInUser(int userId) {
+ try {
+ Uri uri = ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, userId);
+ verify(mContentProvider, timeout(TEST_TIMEOUT_MILLIS).never()).insert(any(String.class),
+ eq(uri), any(ContentValues.class));
+ } catch (android.os.RemoteException e) {
+ fail("Remote exception occurred during test execution");
+ }
+ }
+
+ private ContentValues verifyInsertionWithCapture(int userId) {
+ ArgumentCaptor<ContentValues> captor = ArgumentCaptor.forClass(ContentValues.class);
+ try {
+ Uri uri = ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, userId);
+ verify(mContentProvider, timeout(TEST_TIMEOUT_MILLIS).atLeastOnce())
+ .insert(any(String.class), eq(uri), captor.capture());
+ } catch (android.os.RemoteException e) {
+ fail("Remote exception occurred during test execution");
+ }
+
+ return captor.getValue();
+ }
+
+ private Call makeFakeCall(int disconnectCauseCode, boolean isConference, boolean isIncoming,
+ long creationTimeMillis, long ageMillis, Uri callHandle,
+ PhoneAccountHandle phoneAccountHandle, int callVideoState,
+ String postDialDigits, UserHandle initiatingUser) {
+ return makeFakeCall(disconnectCauseCode, isConference, isIncoming, creationTimeMillis,
+ ageMillis,
+ callHandle, phoneAccountHandle, callVideoState, postDialDigits, initiatingUser,
+ Call.DATA_USAGE_NOT_SET);
+ }
+
+ private Call makeFakeCall(int disconnectCauseCode, boolean isConference, boolean isIncoming,
+ long creationTimeMillis, long ageMillis, Uri callHandle,
+ PhoneAccountHandle phoneAccountHandle, int callVideoState,
+ String postDialDigits, UserHandle initiatingUser, long callDataUsage) {
+ Call fakeCall = mock(Call.class);
+ when(fakeCall.getDisconnectCause()).thenReturn(
+ new DisconnectCause(disconnectCauseCode));
+ when(fakeCall.isConference()).thenReturn(isConference);
+ when(fakeCall.isIncoming()).thenReturn(isIncoming);
+ when(fakeCall.getCreationTimeMillis()).thenReturn(creationTimeMillis);
+ when(fakeCall.getAgeMillis()).thenReturn(ageMillis);
+ when(fakeCall.getOriginalHandle()).thenReturn(callHandle);
+ when(fakeCall.getTargetPhoneAccount()).thenReturn(phoneAccountHandle);
+ when(fakeCall.getVideoStateHistory()).thenReturn(callVideoState);
+ when(fakeCall.getPostDialDigits()).thenReturn(postDialDigits);
+ when(fakeCall.getInitiatingUser()).thenReturn(initiatingUser);
+ when(fakeCall.getCallDataUsage()).thenReturn(callDataUsage);
+ return fakeCall;
+ }
+
+ private PhoneAccount makeFakePhoneAccount(PhoneAccountHandle phoneAccountHandle,
+ int capabilities) {
+ return PhoneAccount.builder(phoneAccountHandle, "testing")
+ .setCapabilities(capabilities).build();
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java b/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java
index 1ebf170..ea18560 100644
--- a/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java
+++ b/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java
@@ -24,6 +24,7 @@
import android.content.Context;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -55,7 +56,7 @@
}
};
- final List<Request> mRequests = new ArrayList<>();
+ final List<Request> mRequests = Collections.synchronizedList(new ArrayList<Request>());
public CallerInfoAsyncQueryFactoryFixture() throws Exception {
Log.i(this, "Creating ...");
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 9f66f00..7ac324e 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -53,6 +53,8 @@
import android.telecom.ConnectionService;
import android.telecom.InCallService;
import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.test.mock.MockContext;
@@ -60,6 +62,7 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -69,6 +72,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -90,6 +94,21 @@
}
@Override
+ public String getPackageName() {
+ return "com.android.server.telecom.tests";
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ return "/tmp/i/dont/know";
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mApplicationContextSpy;
+ }
+
+ @Override
public File getFilesDir() {
try {
return File.createTempFile("temp", "temp").getParentFile();
@@ -153,12 +172,24 @@
return mUserManager;
case Context.TELEPHONY_SUBSCRIPTION_SERVICE:
return mSubscriptionManager;
+ case Context.TELECOM_SERVICE:
+ return mTelecomManager;
+ case Context.CARRIER_CONFIG_SERVICE:
+ return mCarrierConfigManager;
default:
return null;
}
}
@Override
+ public String getSystemServiceName(Class<?> svcClass) {
+ if (svcClass == UserManager.class) {
+ return Context.USER_SERVICE;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public int getUserId() {
return 0;
}
@@ -170,7 +201,7 @@
@Override
public String getOpPackageName() {
- return "test";
+ return "com.android.server.telecom.tests";
}
@Override
@@ -179,7 +210,7 @@
@Override
protected IContentProvider acquireProvider(Context c, String name) {
Log.i(this, "acquireProvider %s", name);
- return mock(IContentProvider.class);
+ return mContentProvider;
}
@Override
@@ -190,7 +221,7 @@
@Override
protected IContentProvider acquireUnstableProvider(Context c, String name) {
Log.i(this, "acquireUnstableProvider %s", name);
- return mock(IContentProvider.class);
+ return mContentProvider;
}
@Override
@@ -211,6 +242,12 @@
}
@Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ return null;
+ }
+
+ @Override
public void sendBroadcast(Intent intent) {
// TODO -- need to ensure this is captured
}
@@ -221,6 +258,11 @@
}
@Override
+ public void sendBroadcastAsUser(Intent intent, UserHandle userHandle) {
+ // TODO -- need to ensure this is captured
+ }
+
+ @Override
public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
int initialCode, String initialData, Bundle initialExtras) {
@@ -238,13 +280,30 @@
throws PackageManager.NameNotFoundException {
return this;
}
- };
+
+ @Override
+ public int checkCallingOrSelfPermission(String permission) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+
+ @Override
+ public void enforceCallingOrSelfPermission(String permission, String message) {
+ // Don't bother enforcing anything in mock.
+ }
+
+ @Override
+ public void startActivityAsUser(Intent intent, UserHandle userHandle) {
+ // For capturing
+ }
+ }
public class FakeAudioManager extends AudioManager {
private boolean mMute = false;
private boolean mSpeakerphoneOn = false;
+ private int mAudioStreamValue = 1;
private int mMode = AudioManager.MODE_NORMAL;
+ private int mRingerMode = AudioManager.RINGER_MODE_NORMAL;
public FakeAudioManager(Context context) {
super(context);
@@ -279,6 +338,26 @@
public int getMode() {
return mMode;
}
+
+ @Override
+ public void setRingerModeInternal(int ringerMode) {
+ mRingerMode = ringerMode;
+ }
+
+ @Override
+ public int getRingerModeInternal() {
+ return mRingerMode;
+ }
+
+ @Override
+ public void setStreamVolume(int streamTypeUnused, int index, int flagsUnused){
+ mAudioStreamValue = index;
+ }
+
+ @Override
+ public int getStreamVolume(int streamValueUnused) {
+ return mAudioStreamValue;
+ }
}
private final Multimap<String, ComponentName> mComponentNamesByAction =
@@ -317,8 +396,12 @@
private final UserManager mUserManager = mock(UserManager.class);
private final StatusBarManager mStatusBarManager = mock(StatusBarManager.class);
private final SubscriptionManager mSubscriptionManager = mock(SubscriptionManager.class);
+ private final CarrierConfigManager mCarrierConfigManager = mock(CarrierConfigManager.class);
+ private final IContentProvider mContentProvider = mock(IContentProvider.class);
private final Configuration mResourceConfiguration = new Configuration();
+ private TelecomManager mTelecomManager = null;
+
public ComponentContextFixture() {
MockitoAnnotations.initMocks(this);
when(mResources.getConfiguration()).thenReturn(mResourceConfiguration);
@@ -347,6 +430,8 @@
when(mTelephonyManager.getSubIdForPhoneAccount((PhoneAccount) any())).thenReturn(1);
+ when(mTelephonyManager.getNetworkOperatorName()).thenReturn("label1");
+
doAnswer(new Answer<Void>(){
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
@@ -357,6 +442,9 @@
when(mNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
when(mUserManager.getSerialNumberForUser(any(UserHandle.class))).thenReturn(-1L);
+
+ doReturn(null).when(mApplicationContextSpy).registerReceiver(any(BroadcastReceiver.class),
+ any(IntentFilter.class));
}
@Override
@@ -388,8 +476,24 @@
mServiceInfoByComponentName.put(componentName, serviceInfo);
}
- public void putResource(int id, String value) {
+ public void putResource(int id, final String value) {
+ when(mResources.getText(eq(id))).thenReturn(value);
when(mResources.getString(eq(id))).thenReturn(value);
+ when(mResources.getString(eq(id), any())).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ return String.format(value, Arrays.copyOfRange(args, 1, args.length));
+ }
+ });
+ }
+
+ public void putBooleanResource(int id, boolean value) {
+ when(mResources.getBoolean(eq(id))).thenReturn(value);
+ }
+
+ public void setTelecomManager(TelecomManager telecomManager) {
+ mTelecomManager = telecomManager;
}
private void addService(String action, ComponentName name, IInterface service) {
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 903028c..17a4d90 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -27,19 +27,24 @@
import org.mockito.Mockito;
import android.content.ComponentName;
+import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.telecom.CallAudioState;
+import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import java.lang.Override;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -47,13 +52,82 @@
import java.util.Map;
import java.util.Set;
-import static org.mockito.Matchers.any;
-
/**
* Controls a test {@link IConnectionService} as would be provided by a source of connectivity
* to the Telecom framework.
*/
public class ConnectionServiceFixture implements TestFixture<IConnectionService> {
+ static int INVALID_VIDEO_STATE = -1;
+
+ /**
+ * Implementation of ConnectionService that performs no-ops for tasks normally meant for
+ * Telephony and reports success back to Telecom
+ */
+ public class FakeConnectionServiceDelegate extends ConnectionService {
+ int mVideoState = INVALID_VIDEO_STATE;
+
+ @Override
+ public Connection onCreateUnknownConnection(
+ PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
+ return new FakeConnection(request.getVideoState(), request.getAddress());
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
+ return new FakeConnection(
+ mVideoState == INVALID_VIDEO_STATE ? request.getVideoState() : mVideoState,
+ request.getAddress());
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
+ return new FakeConnection(request.getVideoState(), request.getAddress());
+ }
+
+ @Override
+ public void onConference(Connection cxn1, Connection cxn2) {
+ // Usually, this is implemented by something in Telephony, which does a bunch of radio
+ // work to conference the two connections together. Here we just short-cut that and
+ // declare them conferenced.
+ Conference fakeConference = new FakeConference();
+ fakeConference.addConnection(cxn1);
+ fakeConference.addConnection(cxn2);
+ addConference(fakeConference);
+ }
+ }
+
+ public class FakeConnection extends Connection {
+ public FakeConnection(int videoState, Uri address) {
+ super();
+ int capabilities = getConnectionCapabilities();
+ capabilities |= CAPABILITY_MUTE;
+ capabilities |= CAPABILITY_SUPPORT_HOLD;
+ capabilities |= CAPABILITY_HOLD;
+ setVideoState(videoState);
+ setConnectionCapabilities(capabilities);
+ setActive();
+ setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
+
+ public class FakeConference extends Conference {
+ public FakeConference() {
+ super(null);
+ setConnectionCapabilities(
+ Connection.CAPABILITY_SUPPORT_HOLD
+ | Connection.CAPABILITY_HOLD
+ | Connection.CAPABILITY_MUTE
+ | Connection.CAPABILITY_MANAGE_CONFERENCE);
+ }
+
+ @Override
+ public void onMerge(Connection connection) {
+ // Do nothing besides inform the connection that it was merged into this conference.
+ connection.setConference(this);
+ }
+ }
public class FakeConnectionService extends IConnectionService.Stub {
@@ -63,6 +137,7 @@
if (!mConnectionServiceAdapters.add(adapter)) {
throw new RuntimeException("Adapter already added: " + adapter);
}
+ mConnectionServiceDelegateAdapter.addConnectionServiceAdapter(adapter);
}
@Override
@@ -71,6 +146,7 @@
if (!mConnectionServiceAdapters.remove(adapter)) {
throw new RuntimeException("Adapter never added: " + adapter);
}
+ mConnectionServiceDelegateAdapter.removeConnectionServiceAdapter(adapter);
}
@Override
@@ -91,7 +167,12 @@
c.isIncoming = isIncoming;
c.isUnknown = isUnknown;
c.capabilities |= Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD;
+ c.videoState = request.getVideoState();
+ c.mockVideoProvider = new MockVideoProvider();
+ c.videoProvider = c.mockVideoProvider.getInterface();
mConnectionById.put(id, c);
+ mConnectionServiceDelegateAdapter.createConnection(connectionManagerPhoneAccount,
+ id, request, isIncoming, isUnknown);
}
@Override
@@ -113,6 +194,9 @@
public void disconnect(String callId) throws RemoteException { }
@Override
+ public void silence(String callId) throws RemoteException { }
+
+ @Override
public void hold(String callId) throws RemoteException { }
@Override
@@ -129,7 +213,9 @@
public void stopDtmfTone(String callId) throws RemoteException { }
@Override
- public void conference(String conferenceCallId, String callId) throws RemoteException { }
+ public void conference(String conferenceCallId, String callId) throws RemoteException {
+ mConnectionServiceDelegateAdapter.conference(conferenceCallId, callId);
+ }
@Override
public void splitFromConference(String callId) throws RemoteException { }
@@ -152,7 +238,12 @@
public IInterface queryLocalInterface(String descriptor) {
return this;
}
- };
+ }
+
+ FakeConnectionServiceDelegate mConnectionServiceDelegate =
+ new FakeConnectionServiceDelegate();
+ private IConnectionService mConnectionServiceDelegateAdapter =
+ IConnectionService.Stub.asInterface(mConnectionServiceDelegate.onBind(null));
private IConnectionService.Stub mConnectionService = new FakeConnectionService();
private IConnectionService.Stub mConnectionServiceSpy = Mockito.spy(mConnectionService);
@@ -174,6 +265,8 @@
int callerDisplayNamePresentation;
final List<String> conferenceableConnectionIds = new ArrayList<>();
IVideoProvider videoProvider;
+ Connection.VideoProvider videoProviderImpl;
+ MockVideoProvider mockVideoProvider;
int videoState;
boolean isVoipAudioMode;
Bundle extras;
diff --git a/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java b/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
new file mode 100644
index 0000000..363d613
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.ContactsAsyncHelper;
+
+import org.mockito.ArgumentCaptor;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+public class ContactsAsyncHelperTest extends TelecomTestCase {
+ private static final Uri SAMPLE_CONTACT_PHOTO_URI = Uri.parse(
+ "android.resource://com.android.server.telecom.tests/"
+ + R.drawable.contacts_sample_photo);
+
+ private static final Uri SAMPLE_CONTACT_PHOTO_URI_SMALL = Uri.parse(
+ "android.resource://com.android.server.telecom.tests/"
+ + R.drawable.contacts_sample_photo_small);
+
+ private static final int TOKEN = 4847524;
+ private static final int TEST_TIMEOUT = 500;
+ private static final Object COOKIE = new Object();
+
+ public static class ImageLoadListenerImpl
+ implements ContactsAsyncHelper.OnImageLoadCompleteListener {
+ @Override
+ public void onImageLoadComplete(int token, Drawable photo,
+ Bitmap photoIcon, Object cookie) {
+ }
+ }
+
+ private ImageLoadListenerImpl mListener = spy(new ImageLoadListenerImpl());
+
+ private ContactsAsyncHelper.ContentResolverAdapter mWorkingContentResolverAdapter =
+ new ContactsAsyncHelper.ContentResolverAdapter() {
+ @Override
+ public InputStream openInputStream(Context context, Uri uri)
+ throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(uri);
+ }
+ };
+
+ private ContactsAsyncHelper.ContentResolverAdapter mNullContentResolverAdapter =
+ new ContactsAsyncHelper.ContentResolverAdapter() {
+ @Override
+ public InputStream openInputStream(Context context, Uri uri)
+ throws FileNotFoundException {
+ return null;
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ mContext = getTestContext();
+ super.setUp();
+ }
+
+ @SmallTest
+ public void testEmptyUri() {
+ ContactsAsyncHelper cah = new ContactsAsyncHelper(mNullContentResolverAdapter);
+ try {
+ cah.startObtainPhotoAsync(TOKEN, mContext, null, mListener, COOKIE);
+ } catch (IllegalStateException e) {
+ // expected to fail
+ }
+ verify(mListener, timeout(TEST_TIMEOUT).never()).onImageLoadComplete(anyInt(),
+ any(Drawable.class), any(Bitmap.class), anyObject());
+ }
+
+ @SmallTest
+ public void testNullReturnFromOpenInputStream() {
+ ContactsAsyncHelper cah = new ContactsAsyncHelper(mNullContentResolverAdapter);
+ cah.startObtainPhotoAsync(TOKEN, mContext, SAMPLE_CONTACT_PHOTO_URI, mListener, COOKIE);
+
+ verify(mListener, timeout(TEST_TIMEOUT)).onImageLoadComplete(eq(TOKEN),
+ isNull(Drawable.class), isNull(Bitmap.class), eq(COOKIE));
+ }
+
+ @SmallTest
+ public void testImageScaling() {
+ ContactsAsyncHelper cah = new ContactsAsyncHelper(mWorkingContentResolverAdapter);
+ cah.startObtainPhotoAsync(TOKEN, mContext, SAMPLE_CONTACT_PHOTO_URI, mListener, COOKIE);
+
+ ArgumentCaptor<Drawable> photoCaptor = ArgumentCaptor.forClass(Drawable.class);
+ ArgumentCaptor<Bitmap> iconCaptor = ArgumentCaptor.forClass(Bitmap.class);
+
+ verify(mListener, timeout(TEST_TIMEOUT)).onImageLoadComplete(eq(TOKEN),
+ photoCaptor.capture(), iconCaptor.capture(), eq(COOKIE));
+
+ Bitmap capturedPhoto = ((BitmapDrawable) photoCaptor.getValue()).getBitmap();
+ assertTrue(getExpectedPhoto(SAMPLE_CONTACT_PHOTO_URI).sameAs(capturedPhoto));
+ int iconSize = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.notification_icon_size);
+ assertTrue(iconSize >= iconCaptor.getValue().getHeight());
+ assertTrue(iconSize >= iconCaptor.getValue().getWidth());
+ }
+
+ @SmallTest
+ public void testNoScaling() {
+ ContactsAsyncHelper cah = new ContactsAsyncHelper(mWorkingContentResolverAdapter);
+ cah.startObtainPhotoAsync(TOKEN, mContext, SAMPLE_CONTACT_PHOTO_URI_SMALL,
+ mListener, COOKIE);
+
+ ArgumentCaptor<Drawable> photoCaptor = ArgumentCaptor.forClass(Drawable.class);
+ ArgumentCaptor<Bitmap> iconCaptor = ArgumentCaptor.forClass(Bitmap.class);
+
+ verify(mListener, timeout(TEST_TIMEOUT)).onImageLoadComplete(eq(TOKEN),
+ photoCaptor.capture(), iconCaptor.capture(), eq(COOKIE));
+
+ Bitmap capturedPhoto = ((BitmapDrawable) photoCaptor.getValue()).getBitmap();
+ assertTrue(getExpectedPhoto(SAMPLE_CONTACT_PHOTO_URI_SMALL).sameAs(capturedPhoto));
+ assertTrue(capturedPhoto.sameAs(iconCaptor.getValue()));
+ }
+
+ private Bitmap getExpectedPhoto(Uri uri) {
+ InputStream is;
+ try {
+ is = mContext.getContentResolver().openInputStream(uri);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+
+ Drawable d = Drawable.createFromStream(is, uri.toString());
+ return ((BitmapDrawable) d).getBitmap();
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
new file mode 100644
index 0000000..e8bd05c
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Debug;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIdMapper;
+import com.android.server.telecom.ConnectionServiceRepository;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CreateConnectionProcessor;
+import com.android.server.telecom.CreateConnectionResponse;
+import com.android.server.telecom.PhoneAccountRegistrar;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+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.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit testing for CreateConnectionProcessor as well as CreateConnectionTimeout classes.
+ */
+public class CreateConnectionProcessorTest extends TelecomTestCase {
+
+ private static final String TEST_PACKAGE = "com.android.server.telecom.tests";
+ private static final String TEST_CLASS =
+ "com.android.server.telecom.tests.MockConnectionService";
+
+ @Mock
+ ConnectionServiceRepository mMockConnectionServiceRepository;
+ @Mock
+ PhoneAccountRegistrar mMockAccountRegistrar;
+ @Mock
+ CreateConnectionResponse mMockCreateConnectionResponse;
+ @Mock
+ Call mMockCall;
+
+ CreateConnectionProcessor mTestCreateConnectionProcessor;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+
+ mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
+ mMockConnectionServiceRepository, mMockCreateConnectionResponse,
+ mMockAccountRegistrar, mContext);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mTestCreateConnectionProcessor = null;
+ super.tearDown();
+ }
+
+ public void testSimPhoneAccountSuccess() throws Exception {
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ // No Connection Manager in this case
+ when(mMockAccountRegistrar.getSimCallManagerFromCall(any(Call.class))).thenReturn(null);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+
+ mTestCreateConnectionProcessor.process();
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ // Notify successful connection to call
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ public void testbadPhoneAccount() throws Exception {
+ PhoneAccountHandle pAHandle = null;
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(pAHandle);
+ givePhoneAccountBindPermission(pAHandle);
+ // No Connection Manager in this case
+ when(mMockAccountRegistrar.getSimCallManagerFromCall(any(Call.class))).thenReturn(null);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+
+ mTestCreateConnectionProcessor.process();
+
+ verify(service, never()).createConnection(eq(mMockCall),
+ any(CreateConnectionResponse.class));
+ verify(mMockCreateConnectionResponse).handleCreateConnectionFailure(
+ eq(new DisconnectCause(DisconnectCause.ERROR)));
+ }
+
+ public void testConnectionManagerSuccess() throws Exception {
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ // Include a Connection Manager
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandle("cm_acct");
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ // Make sure the target phone account has the correct permissions
+ PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
+ mFakeTargetPhoneAccount);
+
+ mTestCreateConnectionProcessor.process();
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(eq(callManagerPAHandle));
+ verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ // Notify successful connection to call
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ public void testConnectionManagerFailedFallToSim() throws Exception {
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ // Include a Connection Manager
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandle("cm_acct");
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
+ PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
+ mFakeTargetPhoneAccount);
+ when(mMockCall.getConnectionService()).thenReturn(service);
+ // Put CreateConnectionProcessor in correct state to fail with ConnectionManager
+ mTestCreateConnectionProcessor.process();
+ reset(mMockCall);
+ reset(service);
+
+ // Notify that the ConnectionManager has denied the call.
+ when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
+ when(mMockCall.getConnectionService()).thenReturn(service);
+ mTestCreateConnectionProcessor.handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED));
+
+ // Verify that the Sim Phone Account is used correctly
+ verify(mMockCall).setConnectionManagerPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ // Notify successful connection to call
+ CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+ mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+ verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+ }
+
+ public void testConnectionManagerFailedDoNotFallToSim() throws Exception {
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ when(mMockCall.isEmergencyCall()).thenReturn(false);
+ // Include a Connection Manager
+ PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandle("cm_acct");
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
+ PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
+ mFakeTargetPhoneAccount);
+ when(mMockCall.getConnectionService()).thenReturn(service);
+ // Put CreateConnectionProcessor in correct state to fail with ConnectionManager
+ mTestCreateConnectionProcessor.process();
+ reset(mMockCall);
+ reset(service);
+
+ // Notify that the ConnectionManager has rejected the call.
+ when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
+ when(mMockCall.getConnectionService()).thenReturn(service);
+ when(service.isServiceValid("createConnection")).thenReturn(true);
+ mTestCreateConnectionProcessor.handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.OTHER));
+
+ // Verify call connection rejected
+ verify(mMockCreateConnectionResponse).handleCreateConnectionFailure(
+ new DisconnectCause(DisconnectCause.OTHER));
+ }
+
+ public void testEmergencyCallToSim() throws Exception {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ // Put in a regular phone account to be sure it doesn't call that
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ // Include a Connection Manager to be sure it doesn't call that
+ PhoneAccount callManagerPA = getNewConnectionManagerPhoneAccount("cm_acct", 0);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer");
+ 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);
+ }
+
+ public void testEmergencyCallSimFailToConnectionManager() throws Exception {
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+ when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+ // Put in a regular phone account to be sure it doesn't call that
+ PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+ when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ any(String.class))).thenReturn(pAHandle);
+ // Include a normal Connection Manager to be sure it doesn't call that
+ PhoneAccount callManagerPA = getNewConnectionManagerPhoneAccount("cm_acct", 0);
+ // Include a connection Manager for the user with the capability to make calls
+ PhoneAccount emerCallManagerPA = getNewEmergencyConnectionManagerPhoneAccount("cm_acct",
+ PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+ ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+ PhoneAccount emergencyPhoneAccount = makeEmergencyPhoneAccount("tel_emer");
+ mTestCreateConnectionProcessor.process();
+ reset(mMockCall);
+ reset(service);
+ when(mMockCall.isEmergencyCall()).thenReturn(true);
+
+ // When Notify SIM connection fails, fall back to connection manager
+ mTestCreateConnectionProcessor.handleCreateConnectionFailure(new DisconnectCause(
+ DisconnectCause.REJECTED));
+
+ verify(mMockCall).setConnectionManagerPhoneAccount(
+ eq(emerCallManagerPA.getAccountHandle()));
+ verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+ verify(mMockCall).setConnectionService(eq(service));
+ verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+ }
+
+ private PhoneAccount makeEmergencyPhoneAccount(String id) {
+ final PhoneAccount emergencyPhoneAccount = makeQuickAccount(id,
+ PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS |
+ PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+ PhoneAccountHandle emergencyPhoneAccountHandle = emergencyPhoneAccount.getAccountHandle();
+ givePhoneAccountBindPermission(emergencyPhoneAccountHandle);
+ ArrayList<PhoneAccount> phoneAccounts = new ArrayList<PhoneAccount>() {{
+ add(emergencyPhoneAccount);
+ }};
+ when(mMockAccountRegistrar.getAllPhoneAccountsOfCurrentUser()).thenReturn(phoneAccounts);
+ return emergencyPhoneAccount;
+ }
+
+ private void givePhoneAccountBindPermission(PhoneAccountHandle handle) {
+ when(mMockAccountRegistrar.phoneAccountRequiresBindPermission(eq(handle))).thenReturn(true);
+ }
+
+ private PhoneAccountHandle getNewConnectionMangerHandle(String id) {
+ PhoneAccountHandle callManagerPAHandle = makeQuickAccountHandle(id);
+ when(mMockAccountRegistrar.getSimCallManagerFromCall(any(Call.class))).thenReturn(
+ callManagerPAHandle);
+ givePhoneAccountBindPermission(callManagerPAHandle);
+ return callManagerPAHandle;
+ }
+
+ private PhoneAccountHandle getNewTargetPhoneAccountHandle(String id) {
+ PhoneAccountHandle pAHandle = makeQuickAccountHandle(id);
+ when(mMockCall.getTargetPhoneAccount()).thenReturn(pAHandle);
+ givePhoneAccountBindPermission(pAHandle);
+ return pAHandle;
+ }
+
+ private PhoneAccount getNewConnectionManagerPhoneAccount(String id, int capability) {
+ PhoneAccount callManagerPA = makeQuickAccount(id, capability);
+ when(mMockAccountRegistrar.getSimCallManagerFromCall(any(Call.class))).thenReturn(
+ callManagerPA.getAccountHandle());
+ givePhoneAccountBindPermission(callManagerPA.getAccountHandle());
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(
+ callManagerPA.getAccountHandle())).thenReturn(callManagerPA);
+ return callManagerPA;
+ }
+
+ private PhoneAccount getNewEmergencyConnectionManagerPhoneAccount(String id, int capability) {
+ PhoneAccount callManagerPA = makeQuickAccount(id, capability);
+ when(mMockAccountRegistrar.getSimCallManagerOfCurrentUser()).thenReturn(
+ callManagerPA.getAccountHandle());
+ givePhoneAccountBindPermission(callManagerPA.getAccountHandle());
+ when(mMockAccountRegistrar.getPhoneAccountUnchecked(
+ callManagerPA.getAccountHandle())).thenReturn(callManagerPA);
+ return callManagerPA;
+ }
+
+ private static ComponentName makeQuickConnectionServiceComponentName() {
+ return new ComponentName(TEST_PACKAGE, TEST_CLASS);
+ }
+
+ private ConnectionServiceWrapper makeConnectionServiceWrapper() {
+ ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+ when(mMockConnectionServiceRepository.getService(
+ eq(makeQuickConnectionServiceComponentName()),
+ eq(Binder.getCallingUserHandle()))).thenReturn(wrapper);
+ return wrapper;
+ }
+
+ private static PhoneAccountHandle makeQuickAccountHandle(String id) {
+ return new PhoneAccountHandle(makeQuickConnectionServiceComponentName(), id,
+ Binder.getCallingUserHandle());
+ }
+
+ private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
+ return new PhoneAccount.Builder(makeQuickAccountHandle(id), "label" + idx);
+ }
+
+ private PhoneAccount makeQuickAccount(String id, int idx) {
+ return makeQuickAccountBuilder(id, idx)
+ .setAddress(Uri.parse("http://foo.com/" + idx))
+ .setSubscriptionAddress(Uri.parse("tel:555-000" + idx))
+ .setCapabilities(idx)
+ .setIcon(Icon.createWithResource(
+ "com.android.server.telecom.tests", R.drawable.stat_sys_phone_call))
+ .setShortDescription("desc" + idx)
+ .setIsEnabled(true)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index 2dd4b97..53e58af 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -45,6 +45,7 @@
public boolean mBringToForeground;
public boolean mShowDialpad;
public boolean mCanAddCall;
+ public boolean mSilenceRinger;
public class FakeInCallService extends IInCallService.Stub {
@Override
@@ -105,6 +106,11 @@
}
@Override
+ public void silenceRinger() throws RemoteException {
+ mSilenceRinger = true;
+ }
+
+ @Override
public IBinder asBinder() {
return this;
}
@@ -128,4 +134,8 @@
public ParcelableCall getCall(String id) {
return mCallById.get(id);
}
+
+ public IInCallAdapter getInCallAdapter() {
+ return mInCallAdapter;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java b/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
index bc294aa..76bac9f 100644
--- a/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
@@ -16,91 +16,95 @@
package com.android.server.telecom.tests;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;
-import android.content.Context;
import android.os.PowerManager;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.InCallWakeLockController;
+import com.android.server.telecom.TelecomWakeLock;
import org.mockito.Mock;
-/**
- * TODO: The tests here are disabled because they depend on classes {@code PowerManager} and
- * {@code PowerManager.WakeLock}, which are both {@code final} and therefore cannot easily be
- * mocked.
- *
- * At the moment, we are using an {@link com.android.server.telecom.InCallWakeLockControllerFactory}
- * in the system under test to abstract out this class, and are assuming it's simple enough that it
- * is not the highest priority to test.
- */
public class InCallWakeLockControllerTest extends TelecomTestCase {
- @Mock Context mContext;
- @Mock PowerManager mPowerManager;
- @Mock PowerManager.WakeLock mWakeLock;
@Mock CallsManager mCallsManager;
@Mock Call mCall;
-
+ @Mock TelecomWakeLock.WakeLockAdapter mWakeLockAdapter;
private InCallWakeLockController mInCallWakeLockController;
@Override
public void setUp() throws Exception {
- /*
super.setUp();
-
- when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
- when(mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "InCallWakeLockController"))
- .thenReturn(mWakeLock);
- mInCallWakeLockController = new InCallWakeLockController(mContext, mCallsManager);
- */
+ TelecomWakeLock telecomWakeLock = new TelecomWakeLock(
+ null, //context never used due to mock WakeLockAdapter
+ mWakeLockAdapter, PowerManager.FULL_WAKE_LOCK,
+ InCallWakeLockControllerTest.class.getSimpleName());
+ mInCallWakeLockController = new InCallWakeLockController(telecomWakeLock, mCallsManager);
}
@Override
public void tearDown() throws Exception {
- /*
+ mInCallWakeLockController = null;
super.tearDown();
- */
}
- public void DONT_test_RingingCallAdded() throws Exception {
+ public void testRingingCallAdded() throws Exception {
when(mCallsManager.getRingingCall()).thenReturn(mCall);
+ when(mWakeLockAdapter.isHeld()).thenReturn(false);
+
mInCallWakeLockController.onCallAdded(mCall);
- verify(mWakeLock).acquire();
+
+ verify(mWakeLockAdapter).acquire();
}
- public void DONT_test_NonRingingCallAdded() throws Exception {
+ public void testNonRingingCallAdded() throws Exception {
when(mCallsManager.getRingingCall()).thenReturn(null);
- when(mWakeLock.isHeld()).thenReturn(false);
+ when(mWakeLockAdapter.isHeld()).thenReturn(false);
mInCallWakeLockController.onCallAdded(mCall);
- verify(mWakeLock, never()).acquire();
+
+ verify(mWakeLockAdapter, never()).acquire();
}
- public void DONT_test_RingingCallTransition() throws Exception {
+ public void testRingingCallTransition() throws Exception {
when(mCallsManager.getRingingCall()).thenReturn(mCall);
+ when(mWakeLockAdapter.isHeld()).thenReturn(false);
+
mInCallWakeLockController.onCallStateChanged(mCall, CallState.NEW, CallState.RINGING);
- verify(mWakeLock).acquire();
+
+ verify(mWakeLockAdapter).acquire();
}
- public void DONT_test_RingingCallRemoved() throws Exception {
+ public void testRingingCallRemoved() throws Exception {
when(mCallsManager.getRingingCall()).thenReturn(null);
- when(mWakeLock.isHeld()).thenReturn(false);
+ when(mWakeLockAdapter.isHeld()).thenReturn(false);
mInCallWakeLockController.onCallRemoved(mCall);
- verify(mWakeLock, never()).acquire();
+
+ verify(mWakeLockAdapter, never()).acquire();
}
- public void DONT_test_WakeLockReleased() throws Exception {
+ public void testWakeLockReleased() throws Exception {
when(mCallsManager.getRingingCall()).thenReturn(null);
- when(mWakeLock.isHeld()).thenReturn(true);
+ when(mWakeLockAdapter.isHeld()).thenReturn(true);
mInCallWakeLockController.onCallRemoved(mCall);
- verify(mWakeLock).release();
+
+ verify(mWakeLockAdapter).release(0);
+ }
+
+ public void testAcquireWakeLockWhenHeld() throws Exception {
+ when(mCallsManager.getRingingCall()).thenReturn(mCall);
+ when(mWakeLockAdapter.isHeld()).thenReturn(true);
+
+ mInCallWakeLockController.onCallAdded(mock(Call.class));
+
+ verify(mWakeLockAdapter, never()).acquire();
}
}
diff --git a/tests/src/com/android/server/telecom/tests/LogTest.java b/tests/src/com/android/server/telecom/tests/LogTest.java
new file mode 100644
index 0000000..d6c534e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/LogTest.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.Runnable;
+import com.android.server.telecom.Session;
+import com.android.server.telecom.SystemLoggingContainer;
+import com.android.server.telecom.Log;
+
+import org.junit.Assert;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Unit tests for Telecom's Logging system.
+ */
+public class LogTest extends TelecomTestCase{
+
+ /**
+ * This helper class captures the logs that are sent to Log and stores them in an array to be
+ * verified by LogTest.
+ */
+ private class TestLoggingContainer extends SystemLoggingContainer {
+ public ArrayList<String> receivedStrings;
+
+ public TestLoggingContainer() {
+ receivedStrings = new ArrayList<>(100);
+ }
+
+ @Override
+ public synchronized void v(String msgTag, String msg) {
+ if (msgTag.equals(LogTest.TESTING_TAG)) {
+ synchronized (this) {
+ receivedStrings.add(processMessage(msg));
+ }
+ }
+ }
+
+ @Override
+ public synchronized void i(String msgTag, String msg) {
+ if (msgTag.equals(LogTest.TESTING_TAG)) {
+ synchronized (this) {
+ receivedStrings.add(processMessage(msg));
+ }
+ }
+ }
+
+ public boolean didReceiveMessage(int timeoutMs, String msg) {
+ String matchedString = null;
+ // Wait for timeout to expire before checking received messages
+ if (timeoutMs > 0) {
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Log.w(LogTest.TESTING_TAG, "TestLoggingContainer: Thread Interrupted!");
+ }
+ }
+ synchronized (this) {
+ for (String receivedString : receivedStrings) {
+ if (receivedString.contains(msg)) {
+ matchedString = receivedString;
+ break;
+ }
+ }
+ if (matchedString != null) {
+ receivedStrings.remove(matchedString);
+ return true;
+ }
+ }
+ android.util.Log.i(TESTING_TAG, "Did not receive message: " + msg);
+ return false;
+ }
+
+ public boolean isMessagesEmpty() {
+ boolean isEmpty = receivedStrings.isEmpty();
+ if (!isEmpty) {
+ printMessagesThatAreLeft();
+ }
+ return isEmpty;
+ }
+
+ public synchronized void printMessagesThatAreLeft() {
+ android.util.Log.i(TESTING_TAG, "Remaining Messages in Log Queue:");
+ for (String receivedString : receivedStrings) {
+ android.util.Log.i(TESTING_TAG, "\t- " + receivedString);
+ }
+ }
+
+ // Remove Unnecessary parts of message string before processing
+ private String processMessage(String msg) {
+ if(msg.contains(Session.CREATE_SUBSESSION)) {
+ return clipMsg(Session.CREATE_SUBSESSION, msg);
+ }
+ if(msg.contains(Session.CONTINUE_SUBSESSION)) {
+ return clipMsg(Session.CONTINUE_SUBSESSION, msg);
+ }
+ if (msg.contains(Session.END_SUBSESSION)) {
+ return clipMsg(Session.END_SUBSESSION, msg);
+ }
+ if (msg.contains(Session.END_SESSION)) {
+ return clipMsg(Session.END_SESSION, msg);
+ }
+ return msg;
+ }
+
+ private String clipMsg(String id, String msg) {
+ int clipStartIndex = msg.indexOf(id) + id.length();
+ int clipEndIndex = msg.lastIndexOf(":");
+ return msg.substring(0, clipStartIndex) + msg.substring(clipEndIndex, msg.length());
+ }
+ }
+
+ public static final int TEST_THREAD_COUNT = 150;
+ public static final int TEST_SLEEP_TIME_MS = 50;
+ // Should be larger than TEST_SLEEP_TIME_MS!
+ public static final int TEST_VERIFY_TIMEOUT_MS = 100;
+ public static final String TEST_ENTER_METHOD1 = "TEM1";
+ public static final String TEST_ENTER_METHOD2 = "TEM2";
+ public static final String TEST_ENTER_METHOD3 = "TEM3";
+ public static final String TEST_ENTER_METHOD4 = "TEM4";
+ public static final String TEST_CLASS_NAME = "LogTest";
+
+ private static final int EVENT_START_TEST_SLEEPY_METHOD = 0;
+ private static final int EVENT_START_TEST_SLEEPY_MULTIPLE_METHOD = 1;
+ private static final int EVENT_LAST_MESSAGE = 2;
+
+ private static final long RANDOM_NUMBER_SEED = 6191991;
+
+ Random rng = new Random(RANDOM_NUMBER_SEED);
+
+ private Handler mSleepyHandler = new Handler(
+ new HandlerThread("sleepyThread"){{start();}}.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ SomeArgs args = (SomeArgs) msg.obj;
+ Session subsession = (Session) args.arg1;
+ Log.continueSession(subsession, "lTSH.hM");
+ switch (msg.what) {
+ case EVENT_START_TEST_SLEEPY_METHOD:
+ sleepyMethod(TEST_SLEEP_TIME_MS);
+ break;
+ }
+ Log.endSession();
+ }
+ };
+
+ private boolean isHandlerCompleteWithEvents;
+ private Handler mSleepyMultipleHandler = new Handler(
+ new HandlerThread("sleepyMultipleThread"){{start();}}.getLooper()){
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_START_TEST_SLEEPY_MULTIPLE_METHOD:
+ SomeArgs args = (SomeArgs) msg.obj;
+ Session subsession = (Session) args.arg1;
+ Log.continueSession(subsession, "lTSCH.hM");
+ sleepyMultipleMethod();
+ Log.endSession();
+ break;
+ case EVENT_LAST_MESSAGE:
+ isHandlerCompleteWithEvents = true;
+ break;
+ }
+ }
+ };
+
+ private AtomicInteger mCompleteCount;
+ class LogTestRunnable implements java.lang.Runnable {
+ private String mshortMethodName;
+ public LogTestRunnable(String shortMethodName) {
+ mshortMethodName = shortMethodName;
+ }
+
+ public void run() {
+ Log.startSession(mshortMethodName);
+ sleepyCallerMethod(TEST_SLEEP_TIME_MS);
+ Log.endSession();
+ mCompleteCount.incrementAndGet();
+ }
+ }
+
+ TestLoggingContainer mTestSystemLogger;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mTestSystemLogger = new TestLoggingContainer();
+ Log.setLoggingContainer(mTestSystemLogger);
+ Log.setIsExtendedLoggingEnabled(true);
+ Log.restartSessionCounter();
+ Log.sCleanStaleSessions = null;
+ Log.sSessionMapper.clear();
+ Log.setContext(mComponentContextFixture.getTestDouble().getApplicationContext());
+ Log.sSessionCleanupTimeoutMs = new Log.ISessionCleanupTimeoutMs() {
+ @Override
+ // Set to the default value of Timeouts.getStaleSessionCleanupTimeoutMillis without
+ // needing to query.
+ public long get() {
+ return Log.DEFAULT_SESSION_TIMEOUT_MS;
+ }
+ };
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mTestSystemLogger = null;
+ Log.setLoggingContainer(new SystemLoggingContainer());
+ super.tearDown();
+ }
+
+ public void testSingleThreadSession() throws Exception {
+ String sessionName = "LT.sTS";
+ Log.startSession(sessionName);
+ sleepyMethod(TEST_SLEEP_TIME_MS);
+ Log.endSession();
+
+ verifyEventResult(Session.START_SESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyMethodCall("", sessionName, 0, "", TEST_ENTER_METHOD1, TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyEndEventResult(sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testSingleHandlerThreadSession() throws Exception {
+ String sessionName = "LT.tSHTS";
+ Log.startSession(sessionName);
+ Session subsession = Log.createSubsession();
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = subsession;
+ mSleepyHandler.obtainMessage(EVENT_START_TEST_SLEEPY_METHOD, args).sendToTarget();
+ Log.endSession();
+
+ verifyEventResult(Session.START_SESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyContinueEventResult(sessionName, "lTSH.hM", "_0", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyMethodCall(sessionName, "lTSH.hM", 0, "_0", TEST_ENTER_METHOD1,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.END_SUBSESSION, sessionName + "->lTSH.hM", "_0", 0,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyEndEventResult(sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testSpawnMultipleThreadSessions() throws Exception {
+ final String sessionName = "LT.lTR";
+ mCompleteCount = new AtomicInteger(0);
+ for (int i = 0; i < TEST_THREAD_COUNT; i++) {
+ Thread.sleep(10);
+ new Thread(new LogTestRunnable(sessionName)).start();
+ }
+
+ // Poll until all of the threads have completed
+ while (mCompleteCount.get() < TEST_THREAD_COUNT) {
+ Thread.sleep(1000);
+ }
+
+ // Loop through verification separately to spawn threads fast so there is possible overlap
+ // (verifyEventResult(...) delays)
+ for (int i = 0; i < TEST_THREAD_COUNT; i++) {
+ verifyEventResult(Session.START_SESSION, sessionName, "", i, 0);
+ verifyMethodCall("", sessionName, i, "", TEST_ENTER_METHOD2, 0);
+ verifyMethodCall("", sessionName, i, "", TEST_ENTER_METHOD1, 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", i, 0);
+ verifyEndEventResult(sessionName, "", i, 0);
+ }
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testSpawnMultipleThreadMultipleHandlerSession() throws Exception {
+ String sessionName = "LT.tSMTMHS";
+ Log.startSession(sessionName);
+ Session subsession = Log.createSubsession();
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = subsession;
+ mSleepyMultipleHandler.obtainMessage(EVENT_START_TEST_SLEEPY_MULTIPLE_METHOD,
+ args).sendToTarget();
+ Log.endSession();
+
+ verifyEventResult(Session.START_SESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyContinueEventResult(sessionName, "lTSCH.hM", "_0", 0, TEST_VERIFY_TIMEOUT_MS);
+ verifyMethodCall(sessionName, "lTSCH.hM", 0, "_0", TEST_ENTER_METHOD3,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.END_SUBSESSION, sessionName + "->lTSCH.hM", "_0", 0,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName + "->lTSCH.hM", "_0", 0,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyContinueEventResult(sessionName + "->" + "lTSCH.hM", "lTSH.hM", "_0_0", 0,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyMethodCall(sessionName + "->lTSCH.hM", "lTSH.hM", 0, "_0_0", TEST_ENTER_METHOD1,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyEventResult(Session.END_SUBSESSION, sessionName + "->lTSCH.hM->lTSH.hM", "_0_0", 0,
+ TEST_VERIFY_TIMEOUT_MS);
+ verifyEndEventResult(sessionName, "", 0, TEST_VERIFY_TIMEOUT_MS);
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testSpawnMultipleThreadMultipleHandlerSessions() throws Exception {
+ String sessionName = "LT.tSMTMHSs";
+ isHandlerCompleteWithEvents = false;
+ for (int i = 0; i < TEST_THREAD_COUNT; i++) {
+ Log.startSession(sessionName);
+ Session subsession = Log.createSubsession();
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = subsession;
+ mSleepyMultipleHandler.obtainMessage(EVENT_START_TEST_SLEEPY_MULTIPLE_METHOD,
+ args).sendToTarget();
+ Log.endSession();
+ }
+ // Send a message that denotes the last message that is sent. We poll until this message
+ // is processed in order to verify the results without waiting an arbitrary amount of time
+ // (that can change per device).
+ mSleepyMultipleHandler.obtainMessage(EVENT_LAST_MESSAGE).sendToTarget();
+
+ while (!isHandlerCompleteWithEvents) {
+ Thread.sleep(1000);
+ }
+
+ for (int i = 0; i < TEST_THREAD_COUNT; i++) {
+ verifyEventResult(Session.START_SESSION, sessionName, "", i, 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", i, 0);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName, "", i, 0);
+ verifyContinueEventResult(sessionName, "lTSCH.hM", "_0", i, 0);
+ verifyMethodCall(sessionName, "lTSCH.hM", i, "_0", TEST_ENTER_METHOD3, 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName + "->lTSCH.hM", "_0", i, 0);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName + "->lTSCH.hM", "_0", i, 0);
+ verifyContinueEventResult(sessionName + "->" + "lTSCH.hM", "lTSH.hM", "_0_0", i, 0);
+ verifyMethodCall(sessionName + "->lTSCH.hM", "lTSH.hM", i, "_0_0", TEST_ENTER_METHOD1,
+ 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName + "->lTSCH.hM->lTSH.hM", "_0_0",
+ i, 0);
+ verifyEndEventResult(sessionName, "", i, 0);
+ }
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testCancelSubsession() throws Exception {
+ String sessionName = "LT.tCS";
+ Log.startSession(sessionName);
+ Session subsession = Log.createSubsession();
+ Log.cancelSubsession(subsession);
+ Log.endSession();
+
+ verifyEventResult(Session.START_SESSION, sessionName, "", 0, 0);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName, "", 0, 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", 0, 0);
+ verifyEndEventResult(sessionName, "", 0, 0);
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testInternalExternalCallToMethod() throws Exception {
+ String sessionName = "LT.tIECTM";
+ Log.startSession(sessionName);
+ internalExternalMethod();
+ Log.endSession();
+
+ verifyEventResult(Session.START_SESSION, sessionName, "", 0, 0);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName, "", 0, 0);
+ verifyContinueEventResult(sessionName, "", "", 0, 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", 0, 0);
+ verifyMethodCall("", sessionName, 0, "", TEST_ENTER_METHOD4, 0);
+ verifyEventResult(Session.END_SUBSESSION, sessionName, "", 0, 0);
+ verifyEndEventResult(sessionName, "", 0, 0);
+
+ assertEquals(Log.sSessionMapper.size(), 0);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+ }
+
+ public void testGarbageCollectionWithTimeout() throws Exception {
+ String sessionName = "LT.tGCWT";
+
+ // Don't end session (Oops!)
+ Log.startSession(sessionName);
+ internalDanglingMethod();
+ Log.sSessionCleanupHandler.postDelayed(new java.lang.Runnable() {
+ @Override
+ public void run() {
+ android.util.Log.i(TESTING_TAG, "Running Test SessionCleanupHandler method.");
+ Log.cleanupStaleSessions(1000);
+ }
+ }, 1000);
+
+ verifyEventResult(Session.START_SESSION, sessionName, "", 0, 0);
+ verifyEventResult(Session.CREATE_SUBSESSION, sessionName, "", 0, 0);
+ verifyContinueEventResult(sessionName, "", "", 0, 0);
+ verifyMethodCall("", sessionName, 0, "", TEST_ENTER_METHOD4, 0);
+
+ // Verify the session is still active in sSessionMapper
+ assertEquals(Log.sSessionMapper.size(), 1);
+ assertEquals(true, mTestSystemLogger.isMessagesEmpty());
+
+ // Keep a weak reference to the object to check if it eventually gets garbage collected.
+ int threadId = Log.getCallingThreadId();
+ WeakReference<Session> sessionRef = new WeakReference<>(
+ Log.sSessionMapper.get(threadId));
+
+ Thread.sleep(1100);
+ assertEquals(0, Log.sSessionMapper.size());
+ // "Suggest" that the GC collects the now isolated Session and subsession and wait for it
+ // to occur.
+ System.gc();
+ Thread.sleep(1000);
+ assertEquals(null, sessionRef.get());
+ }
+
+ private void verifyMethodCall(String parentSessionName, String methodName, int sessionId,
+ String subsession, String shortMethodName, int timeoutMs) {
+ if (!parentSessionName.isEmpty()){
+ parentSessionName += "->";
+ }
+ boolean isMessageReceived = mTestSystemLogger.didReceiveMessage(timeoutMs,
+ buildExpectedResult(parentSessionName + methodName, sessionId, subsession,
+ shortMethodName));
+
+ assertEquals(true, isMessageReceived);
+ }
+
+ private String buildExpectedSession(String shortMethodName, int sessionId) {
+ return shortMethodName + "@" + Log.getBase64Encoding(sessionId);
+ }
+
+ private String buildExpectedResult(String shortMethodName, int sessionId,
+ String subsessionId, String logText) {
+ return TEST_CLASS_NAME + ": " + logText + ": " +
+ buildExpectedSession(shortMethodName, sessionId) + subsessionId;
+ }
+
+ private void verifyContinueEventResult(String shortOldMethodName, String shortNewMethodName,
+ String subsession, int sessionId, int timeoutMs) {
+ String expectedSession = buildExpectedSession(shortNewMethodName, sessionId);
+ if(!shortNewMethodName.isEmpty()) {
+ shortOldMethodName += "->";
+ }
+ boolean isMessageReceived = mTestSystemLogger.didReceiveMessage(timeoutMs,
+ Session.CONTINUE_SUBSESSION + ": " + shortOldMethodName + expectedSession +
+ subsession);
+ assertEquals(true, isMessageReceived);
+ }
+
+ private void verifyEventResult(String event, String shortMethodName, String subsession,
+ int sessionId, int timeoutMs) {
+ String expectedSession = buildExpectedSession(shortMethodName, sessionId);
+ boolean isMessageReceived = mTestSystemLogger.didReceiveMessage(timeoutMs,event + ": " +
+ expectedSession + subsession);
+ assertEquals(true, isMessageReceived);
+ }
+
+ private void verifyEndEventResult(String shortMethodName, String subsession, int sessionId,
+ int timeoutMs) {
+ String expectedSession = buildExpectedSession(shortMethodName, sessionId);
+ boolean isMessageReceived = mTestSystemLogger.didReceiveMessage(timeoutMs,
+ Session.END_SESSION + ": " + expectedSession + subsession);
+ assertEquals(true, isMessageReceived);
+ }
+
+ private void internalExternalMethod() {
+ Log.startSession("LT.iEM");
+ Log.i(TEST_CLASS_NAME, TEST_ENTER_METHOD4);
+ Log.endSession();
+ }
+
+ private void internalDanglingMethod() {
+ Log.startSession("LT.iEM");
+ Log.i(TEST_CLASS_NAME, TEST_ENTER_METHOD4);
+ }
+
+ private void sleepyCallerMethod(int timeToSleepMs) {
+ Log.i(TEST_CLASS_NAME, TEST_ENTER_METHOD2);
+ try {
+ Thread.sleep(timeToSleepMs);
+ sleepyMethod(rng.nextInt(TEST_SLEEP_TIME_MS));
+ } catch (InterruptedException e) {
+ // This should not happen
+ Assert.fail("Thread sleep interrupted: " + e.getMessage());
+ }
+
+ }
+
+ private void sleepyMultipleMethod() {
+ Log.i(TEST_CLASS_NAME, TEST_ENTER_METHOD3);
+ Session subsession = Log.createSubsession();
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = subsession;
+ mSleepyHandler.obtainMessage(EVENT_START_TEST_SLEEPY_METHOD, args).sendToTarget();
+ try {
+ Thread.sleep(TEST_SLEEP_TIME_MS);
+ } catch (InterruptedException e) {
+ // This should not happen
+ Assert.fail("Thread sleep interrupted: " + e.getMessage());
+ }
+ }
+
+ private void sleepyMethod(int timeToSleepMs) {
+ Log.i(TEST_CLASS_NAME, TEST_ENTER_METHOD1);
+ try {
+ Thread.sleep(timeToSleepMs);
+ } catch (InterruptedException e) {
+ // This should not happen
+ Assert.fail("Thread sleep interrupted: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
new file mode 100644
index 0000000..c997b31
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccount.Builder;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.Constants;
+import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+import com.android.server.telecom.ui.MissedCallNotifierImpl;
+import com.android.server.telecom.ui.MissedCallNotifierImpl.NotificationBuilderFactory;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.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;
+
+public class MissedCallNotifierImplTest extends TelecomTestCase {
+
+ private static final Uri TEL_CALL_HANDLE = Uri.parse("tel:+11915552620");
+ private static final Uri SIP_CALL_HANDLE = Uri.parse("sip:testaddress@testdomain.com");
+ private static final String CALLER_NAME = "Fake Name";
+ private static final String MISSED_CALL_TITLE = "Missed Call";
+ private static final String MISSED_CALLS_TITLE = "Missed Calls";
+ private static final String MISSED_CALLS_MSG = "%s missed calls";
+ private static final String USER_CALL_ACTIVITY_LABEL = "Phone";
+
+ private static final int REQUEST_ID = 0;
+ private static final long CALL_TIMESTAMP;
+ static {
+ CALL_TIMESTAMP = System.currentTimeMillis() - 60 * 1000 * 5;
+ }
+
+ private static final UserHandle PRIMARY_USER = UserHandle.of(0);
+ private static final UserHandle SECONARY_USER = UserHandle.of(12);
+ private static final int NO_CAPABILITY = 0;
+
+ @Mock
+ private NotificationManager mNotificationManager;
+
+ @Mock
+ private PhoneAccountRegistrar mPhoneAccountRegistrar;
+
+ @Mock
+ private TelecomManager mTelecomManager;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mNotificationManager = (NotificationManager) mContext.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ TelephonyManager fakeTelephonyManager = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ when(fakeTelephonyManager.getNetworkCountryIso()).thenReturn("US");
+ doReturn(new ApplicationInfo()).when(mContext).getApplicationInfo();
+ doReturn("com.android.server.telecom.tests").when(mContext).getPackageName();
+
+ mComponentContextFixture.putResource(R.string.notification_missedCallTitle,
+ MISSED_CALL_TITLE);
+ mComponentContextFixture.putResource(R.string.notification_missedCallsTitle,
+ MISSED_CALLS_TITLE);
+ mComponentContextFixture.putResource(R.string.notification_missedCallsMsg,
+ MISSED_CALLS_MSG);
+ mComponentContextFixture.putResource(R.string.userCallActivityLabel,
+ USER_CALL_ACTIVITY_LABEL);
+ mComponentContextFixture.setTelecomManager(mTelecomManager);
+ }
+
+ public void testCancelNotificationInPrimaryUser() {
+ cancelNotificationTestInternal(PRIMARY_USER);
+ }
+
+ public void testCancelNotificationInSecondaryUser() {
+ cancelNotificationTestInternal(SECONARY_USER);
+ }
+
+ private void cancelNotificationTestInternal(UserHandle userHandle) {
+ Notification.Builder builder1 = makeNotificationBuilder("builder1");
+ Notification.Builder builder2 = makeNotificationBuilder("builder2");
+ MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+ makeNotificationBuilderFactory(builder1, builder1, builder2, builder2);
+
+ MissedCallNotifier missedCallNotifier = makeMissedCallNotifier(fakeBuilderFactory,
+ PRIMARY_USER);
+ PhoneAccount phoneAccount = makePhoneAccount(userHandle, NO_CAPABILITY);
+ Call fakeCall = makeFakeCall(TEL_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
+ phoneAccount.getAccountHandle());
+
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.clearMissedCalls(userHandle);
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+
+ ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(
+ Integer.class);
+ verify(mNotificationManager, times(2)).notifyAsUser(isNull(String.class),
+ requestIdCaptor.capture(), any(Notification.class), eq(userHandle));
+ verify(mNotificationManager).cancelAsUser(any(String.class), eq(requestIdCaptor.getValue()),
+ eq(userHandle));
+
+ // Verify that the second call to showMissedCallNotification behaves like it were the first.
+ verify(builder2).setContentText(CALLER_NAME);
+ }
+
+ public void testNotifyMultipleMissedCalls() {
+ Notification.Builder[] builders = new Notification.Builder[4];
+
+ for (int i = 0; i < 4; i++) {
+ builders[i] = makeNotificationBuilder("builder" + Integer.toString(i));
+ }
+
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+ Call fakeCall = makeFakeCall(TEL_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
+ phoneAccount.getAccountHandle());
+
+ MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+ makeNotificationBuilderFactory(builders);
+
+ MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
+ mPhoneAccountRegistrar, fakeBuilderFactory);
+
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+
+ // The following captor is to capture the two notifications that got passed into
+ // notifyAsUser. This distinguishes between the builders used for the full notification
+ // (i.e. the one potentially containing sensitive information, such as phone numbers),
+ // and the builders used for the notifications shown on the lockscreen, which have been
+ // scrubbed of such sensitive info. The notifications which are used as arguments
+ // to notifyAsUser are the versions which contain sensitive information.
+ ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(
+ Notification.class);
+ verify(mNotificationManager, times(2)).notifyAsUser(isNull(String.class), eq(1),
+ notificationArgumentCaptor.capture(), eq(PRIMARY_USER));
+ HashSet<String> privateNotifications = new HashSet<>();
+ for (Notification n : notificationArgumentCaptor.getAllValues()) {
+ privateNotifications.add(n.toString());
+ }
+
+ for (int i = 0; i < 4; i++) {
+ Notification.Builder builder = builders[i];
+ verify(builder).setWhen(CALL_TIMESTAMP);
+ if (i >= 2) {
+ // The builders after the first two are for multiple missed calls. The notification
+ // for subsequent missed calls is expected to be different in terms of the text
+ // contents of the notification, and that is verified here.
+ if (privateNotifications.contains(builder.toString())) {
+ verify(builder).setContentText(String.format(MISSED_CALLS_MSG, 2));
+ verify(builder).setContentTitle(MISSED_CALLS_TITLE);
+ } else {
+ verify(builder).setContentText(MISSED_CALLS_TITLE);
+ verify(builder).setContentTitle(USER_CALL_ACTIVITY_LABEL);
+ }
+ verify(builder, never()).addAction(any(Notification.Action.class));
+ } else {
+ if (privateNotifications.contains(builder.toString())) {
+ verify(builder).setContentText(CALLER_NAME);
+ verify(builder).setContentTitle(MISSED_CALL_TITLE);
+ } else {
+ verify(builder).setContentText(MISSED_CALL_TITLE);
+ verify(builder).setContentTitle(USER_CALL_ACTIVITY_LABEL);
+ }
+ }
+ }
+ }
+
+ public void testNotifySingleCallInPrimaryUser() {
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+ notifySingleCallTestInternal(phoneAccount, PRIMARY_USER);
+ }
+
+ public void testNotifySingleCallInSecondaryUser() {
+ PhoneAccount phoneAccount = makePhoneAccount(SECONARY_USER, NO_CAPABILITY);
+ notifySingleCallTestInternal(phoneAccount, PRIMARY_USER);
+ }
+
+ public void testNotifySingleCallInSecondaryUserWithMultiUserCapability() {
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER,
+ PhoneAccount.CAPABILITY_MULTI_USER);
+ notifySingleCallTestInternal(phoneAccount, PRIMARY_USER);
+ }
+
+ public void testNotifySingleCallWhenCurrentUserIsSecondaryUser() {
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+ notifySingleCallTestInternal(phoneAccount, SECONARY_USER);
+ }
+
+ public void testNotifySingleCall() {
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+ notifySingleCallTestInternal(phoneAccount, PRIMARY_USER);
+ }
+
+ private void notifySingleCallTestInternal(PhoneAccount phoneAccount, UserHandle currentUser) {
+ Notification.Builder builder1 = makeNotificationBuilder("builder1");
+ Notification.Builder builder2 = makeNotificationBuilder("builder2");
+ MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+ makeNotificationBuilderFactory(builder1, builder2);
+
+ MissedCallNotifier missedCallNotifier = makeMissedCallNotifier(fakeBuilderFactory,
+ currentUser);
+
+ Call fakeCall = makeFakeCall(TEL_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
+ phoneAccount.getAccountHandle());
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+
+ ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(
+ Notification.class);
+
+ UserHandle expectedUserHandle;
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
+ expectedUserHandle = currentUser;
+ } else {
+ expectedUserHandle = phoneAccount.getAccountHandle().getUserHandle();
+ }
+ verify(mNotificationManager).notifyAsUser(isNull(String.class), eq(1),
+ notificationArgumentCaptor.capture(), eq((expectedUserHandle)));
+
+ Notification.Builder builder;
+ Notification.Builder publicBuilder;
+
+ if (notificationArgumentCaptor.getValue().toString().equals("builder1")) {
+ builder = builder1;
+ publicBuilder = builder2;
+ } else {
+ builder = builder2;
+ publicBuilder = builder1;
+ }
+
+ verify(builder).setWhen(CALL_TIMESTAMP);
+ verify(publicBuilder).setWhen(CALL_TIMESTAMP);
+
+ verify(builder).setContentText(CALLER_NAME);
+ verify(publicBuilder).setContentText(MISSED_CALL_TITLE);
+
+ verify(builder).setContentTitle(MISSED_CALL_TITLE);
+ verify(publicBuilder).setContentTitle(USER_CALL_ACTIVITY_LABEL);
+
+ // Create two intents that correspond to call-back and respond back with SMS, and assert
+ // that these pending intents have in fact been registered.
+ Intent callBackIntent = new Intent(
+ TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION,
+ TEL_CALL_HANDLE,
+ mContext,
+ TelecomBroadcastReceiver.class);
+ Intent smsIntent = new Intent(
+ TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION,
+ Uri.fromParts(Constants.SCHEME_SMSTO, TEL_CALL_HANDLE.getSchemeSpecificPart(), null),
+ mContext,
+ TelecomBroadcastReceiver.class);
+
+ assertNotNull(PendingIntent.getBroadcast(mContext, REQUEST_ID,
+ callBackIntent, PendingIntent.FLAG_NO_CREATE));
+ assertNotNull(PendingIntent.getBroadcast(mContext, REQUEST_ID,
+ smsIntent, PendingIntent.FLAG_NO_CREATE));
+ }
+
+ public void testNoSmsBackAfterMissedSipCall() {
+ Notification.Builder builder1 = makeNotificationBuilder("builder1");
+ MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+ makeNotificationBuilderFactory(builder1);
+
+ MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
+ mPhoneAccountRegistrar, fakeBuilderFactory);
+ PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+
+ Call fakeCall =
+ makeFakeCall(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
+ phoneAccount.getAccountHandle());
+ missedCallNotifier.showMissedCallNotification(fakeCall);
+
+ // Create two intents that correspond to call-back and respond back with SMS, and assert
+ // that in the case of a SIP call, no SMS intent is generated.
+ Intent callBackIntent = new Intent(
+ TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION,
+ SIP_CALL_HANDLE,
+ mContext,
+ TelecomBroadcastReceiver.class);
+ Intent smsIntent = new Intent(
+ TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION,
+ Uri.fromParts(Constants.SCHEME_SMSTO, SIP_CALL_HANDLE.getSchemeSpecificPart(),
+ null),
+ mContext,
+ TelecomBroadcastReceiver.class);
+
+ assertNotNull(PendingIntent.getBroadcast(mContext, REQUEST_ID,
+ callBackIntent, PendingIntent.FLAG_NO_CREATE));
+ assertNull(PendingIntent.getBroadcast(mContext, REQUEST_ID,
+ smsIntent, PendingIntent.FLAG_NO_CREATE));
+ }
+
+ private Notification.Builder makeNotificationBuilder(String label) {
+ Notification.Builder builder = spy(new Notification.Builder(mContext));
+ Notification notification = mock(Notification.class);
+ when(notification.toString()).thenReturn(label);
+ when(builder.toString()).thenReturn(label);
+ doReturn(notification).when(builder).build();
+ return builder;
+ }
+
+ private Call makeFakeCall(Uri handle, String name, long timestamp,
+ PhoneAccountHandle phoneAccountHandle) {
+ Call fakeCall = mock(Call.class);
+ when(fakeCall.getHandle()).thenReturn(handle);
+ when(fakeCall.getName()).thenReturn(name);
+ when(fakeCall.getCreationTimeMillis()).thenReturn(timestamp);
+ when(fakeCall.getTargetPhoneAccount()).thenReturn(phoneAccountHandle);
+ return fakeCall;
+ }
+
+ private MissedCallNotifierImpl.NotificationBuilderFactory makeNotificationBuilderFactory(
+ Notification.Builder... builders) {
+ MissedCallNotifierImpl.NotificationBuilderFactory builderFactory =
+ mock(MissedCallNotifierImpl.NotificationBuilderFactory.class);
+ when(builderFactory.getBuilder(mContext)).thenReturn(builders[0],
+ Arrays.copyOfRange(builders, 1, builders.length));
+ return builderFactory;
+ }
+
+ private MissedCallNotifier makeMissedCallNotifier(
+ NotificationBuilderFactory fakeBuilderFactory, UserHandle currentUser) {
+ MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
+ mPhoneAccountRegistrar, fakeBuilderFactory);
+ missedCallNotifier.setCurrentUserHandle(currentUser);
+ return missedCallNotifier;
+ }
+
+ private PhoneAccount makePhoneAccount(UserHandle userHandle, int capability) {
+ ComponentName componentName = new ComponentName("com.anything", "com.whatever");
+ PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, "id",
+ userHandle);
+ PhoneAccount.Builder builder = new PhoneAccount.Builder(phoneAccountHandle, "test");
+ builder.setCapabilities(capability);
+ PhoneAccount phoneAccount = builder.build();
+ when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle))
+ .thenReturn(phoneAccount);
+ return phoneAccount;
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/MockVideoProvider.java b/tests/src/com/android/server/telecom/tests/MockVideoProvider.java
new file mode 100644
index 0000000..147d232
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MockVideoProvider.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 org.mockito.Mockito;
+
+import android.net.Uri;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+import android.view.Surface;
+
+/**
+ * Provides a mock implementation of a video provider.
+ */
+public class MockVideoProvider extends VideoProvider {
+ public static final String CAMERA_NONE = "none";
+ public static final String CAMERA_FRONT = "front";
+ public static final String CAMERA_BACK = "back";
+ public static final int CAMERA_FRONT_DIMENSIONS = 1024;
+ public static final int CAMERA_BACK_DIMENSIONS = 2048;
+ public static final long DATA_USAGE = 1024;
+ public static final int PEER_DIMENSIONS = 4096;
+ public static final int DEVICE_ORIENTATION_UNDEFINED = -1;
+ public static final float ZOOM_UNDEFINED = -1.0f;
+
+ private Surface mPreviewSurface = null;
+ private Surface mDisplaySurface = null;
+ private int mDeviceOrientation = DEVICE_ORIENTATION_UNDEFINED;
+ private float mZoom = ZOOM_UNDEFINED;
+ private VideoProfile mSessionModifyResponse = null;
+ private Uri mPauseImage = null;
+
+ /**
+ * Responds to a request to set the camera by reporting the new camera information via the
+ * {@link #changeCameraCapabilities(VideoProfile.CameraCapabilities)} API.
+ *
+ * @param cameraId The id of the camera (use ids as reported by
+ */
+ @Override
+ public void onSetCamera(String cameraId) {
+ handleCameraChange(cameraId);
+ }
+
+ /**
+ * Stores the preview surface set via the {@link VideoCall#setPreviewSurface(Surface)} API for
+ * retrieval using {@link #getPreviewSurface()}.
+ *
+ * @param surface The {@link Surface}.
+ */
+ @Override
+ public void onSetPreviewSurface(Surface surface) {
+ mPreviewSurface = surface;
+ }
+
+ /**
+ * Stores the display surface set via the {@link VideoCall#setDisplaySurface(Surface)} API for
+ * retrieval using {@link #getDisplaySurface()}.
+ *
+ * @param surface The {@link Surface}.
+ */
+ @Override
+ public void onSetDisplaySurface(Surface surface) {
+ mDisplaySurface = surface;
+ }
+
+ /**
+ * Stores the device orientation set via the {@link VideoCall#setDeviceOrientation(int)} API for
+ * retrieval using {@link #getDeviceOrientation()}.
+ *
+ * @param rotation The device orientation, in degrees.
+ */
+ @Override
+ public void onSetDeviceOrientation(int rotation) {
+ mDeviceOrientation = rotation;
+ }
+
+ /**
+ * Stores the zoom level set via the {@link VideoCall#setZoom(float)} API for retrieval using
+ * {@link #getZoom()}.
+ *
+ * @param value The camera zoom.
+ */
+ @Override
+ public void onSetZoom(float value) {
+ mZoom = value;
+ }
+
+ /**
+ * Responds to any incoming session modify request by accepting the requested profile.
+ *
+ * @param fromProfile The video profile prior to the request.
+ * @param toProfile The video profile with the requested changes made.
+ */
+ @Override
+ public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) {
+ super.receiveSessionModifyResponse(VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS,
+ fromProfile, toProfile);
+ }
+
+ /**
+ * Stores session modify responses received via the
+ * {@link VideoCall#sendSessionModifyResponse(VideoProfile)} API for retrieval via
+ * {@link #getSessionModifyResponse()}.
+ *
+ * @param responseProfile The response video profile.
+ */
+ @Override
+ public void onSendSessionModifyResponse(VideoProfile responseProfile) {
+ mSessionModifyResponse = responseProfile;
+ }
+
+ /**
+ * Responds to requests for camera capabilities by reporting the front camera capabilities.
+ */
+ @Override
+ public void onRequestCameraCapabilities() {
+ handleCameraChange(CAMERA_FRONT);
+ }
+
+ /**
+ * Responds to all requests for data usage by reporting {@link #DATA_USAGE}.
+ */
+ @Override
+ public void onRequestConnectionDataUsage() {
+ super.setCallDataUsage(DATA_USAGE);
+ }
+
+ /**
+ * Stores pause image URIs received via the {@link VideoCall#setPauseImage(Uri)} API for
+ * retrieval via {@link #getPauseImage()}.
+ *
+ * @param uri URI of image to display.
+ */
+ @Override
+ public void onSetPauseImage(Uri uri) {
+ mPauseImage = uri;
+ }
+
+ /**
+ * Handles a change to the current camera selection. Responds by reporting the capabilities of
+ * the camera.
+ */
+ private void handleCameraChange(String cameraId) {
+ if (CAMERA_FRONT.equals(cameraId)) {
+ super.changeCameraCapabilities(new VideoProfile.CameraCapabilities(
+ CAMERA_FRONT_DIMENSIONS, CAMERA_FRONT_DIMENSIONS));
+ } else if (CAMERA_BACK.equals(cameraId)) {
+ super.changeCameraCapabilities(new VideoProfile.CameraCapabilities(
+ CAMERA_BACK_DIMENSIONS, CAMERA_BACK_DIMENSIONS));
+ }
+ }
+
+ /**
+ * Retrieves the last preview surface sent to the provider.
+ *
+ * @return the surface.
+ */
+ public Surface getPreviewSurface() {
+ return mPreviewSurface;
+ }
+
+ /**
+ * Retrieves the last display surface sent to the provider.
+ *
+ * @return the surface.
+ */
+ public Surface getDisplaySurface() {
+ return mDisplaySurface;
+ }
+
+ /**
+ * Retrieves the last device orientation sent to the provider.
+ *
+ * @return the orientation.
+ */
+ public int getDeviceOrientation() {
+ return mDeviceOrientation;
+ }
+
+ /**
+ * Retrieves the last zoom sent to the provider.
+ *
+ * @return the zoom.
+ */
+ public float getZoom() {
+ return mZoom;
+ }
+
+ /**
+ * Retrieves the last session modify response sent to the provider.
+ *
+ * @return the session modify response.
+ */
+ public VideoProfile getSessionModifyResponse() {
+ return mSessionModifyResponse;
+ }
+
+ /**
+ * Retrieves the last pause image sent to the provider.
+ *
+ * @return the pause image URI.
+ */
+ public Uri getPauseImage() {
+ return mPauseImage;
+ }
+
+ /**
+ * Sends a mock session modify request via the provider.
+ */
+ public void sendMockSessionModifyRequest() {
+ super.receiveSessionModifyRequest(new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL));
+ }
+
+ /**
+ * Sends a mock session event via the provider.
+ *
+ * @param event the event.
+ */
+ public void sendMockSessionEvent(int event) {
+ super.handleCallSessionEvent(event);
+ }
+
+ /**
+ * Sends a mock peer dimension change via the provider.
+ *
+ * @param width The new width.
+ * @param height The new height.
+ */
+ public void sendMockPeerDimensions(int width, int height) {
+ super.changePeerDimensions(width, height);
+ }
+
+ /**
+ * Sends a mock video quality change via the provider.
+ *
+ * @param videoQuality the video quality.
+ */
+ public void sendMockVideoQuality(int videoQuality) {
+ super.changeVideoQuality(videoQuality);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/MockitoHelper.java b/tests/src/com/android/server/telecom/tests/MockitoHelper.java
index 5193bba..3425b0e 100644
--- a/tests/src/com/android/server/telecom/tests/MockitoHelper.java
+++ b/tests/src/com/android/server/telecom/tests/MockitoHelper.java
@@ -18,9 +18,7 @@
import com.android.server.telecom.Log;
-import android.annotation.TargetApi;
import android.content.Context;
-import android.os.Looper;
/**
* Helper for Mockito-based test cases.
@@ -28,10 +26,6 @@
public final class MockitoHelper {
private static final String DEXCACHE = "dexmaker.dexcache";
- private Thread mRequestThread;
- private ClassLoader mRequestThreadOriginalClassLoader;
- private ClassLoader mMainThreadOriginalClassLoader;
-
/**
* Creates a new helper, which in turn will set the context classloader so
* it can load Mockito resources.
@@ -39,24 +33,6 @@
* @param packageClass test case class
*/
public void setUp(Context context, Class<?> packageClass) throws Exception {
- // makes a copy of the context classloader
- mRequestThread = Thread.currentThread();
- mRequestThreadOriginalClassLoader = mRequestThread.getContextClassLoader();
- mMainThreadOriginalClassLoader = Looper.getMainLooper().getThread().getContextClassLoader();
-
- ClassLoader newClassLoader = packageClass.getClassLoader();
-
- Log.v(this, "Changing context classloader for thread %s from %s to %s",
- mRequestThread.getName(),
- mRequestThreadOriginalClassLoader,
- newClassLoader);
- mRequestThread.setContextClassLoader(newClassLoader);
-
- Log.v(this, "Changing context classloader for MAIN thread from %s to %s",
- mMainThreadOriginalClassLoader,
- newClassLoader);
- Looper.getMainLooper().getThread().setContextClassLoader(newClassLoader);
-
String dexCache = context.getCacheDir().toString();
Log.v(this, "Setting property %s to %s", DEXCACHE, dexCache);
System.setProperty(DEXCACHE, dexCache);
@@ -67,7 +43,6 @@
*/
public void tearDown() throws Exception {
Log.v(this, "Restoring context classloaders");
- mRequestThread.setContextClassLoader(mRequestThreadOriginalClassLoader);
Log.v(this, "Clearing property %s", DEXCACHE);
System.clearProperty(DEXCACHE);
}
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
new file mode 100644
index 0000000..17ca5b9
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Manifest;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.telecom.GatewayInfo;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.NewOutgoingCallIntentBroadcaster;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNotNull;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
+ private static class ReceiverIntentPair {
+ public BroadcastReceiver receiver;
+ public Intent intent;
+
+ public ReceiverIntentPair(BroadcastReceiver receiver, Intent intent) {
+ this.receiver = receiver;
+ this.intent = intent;
+ }
+ }
+
+ @Mock private CallsManager mCallsManager;
+ @Mock private Call mCall;
+
+ private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapterSpy;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mPhoneNumberUtilsAdapterSpy = spy(new PhoneNumberUtilsAdapterImpl());
+ }
+
+ @SmallTest
+ public void testNullHandle() {
+ Intent intent = new Intent(Intent.ACTION_CALL, null);
+ int result = processIntent(intent, true);
+ assertEquals(DisconnectCause.INVALID_NUMBER, result);
+ verifyNoBroadcastSent();
+ verifyNoCallPlaced();
+ }
+
+ @SmallTest
+ public void testVoicemailCall() {
+ String voicemailNumber = "voicemail:18005551234";
+ Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse(voicemailNumber));
+ intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, true);
+
+ int result = processIntent(intent, true);
+
+ assertEquals(DisconnectCause.NOT_DISCONNECTED, result);
+ verify(mCallsManager).placeOutgoingCall(eq(mCall), eq(Uri.parse(voicemailNumber)),
+ any(GatewayInfo.class), eq(true), eq(VideoProfile.STATE_AUDIO_ONLY));
+ }
+
+ @SmallTest
+ public void testVoicemailCallWithBadAction() {
+ badCallActionHelper(Uri.parse("voicemail:18005551234"), DisconnectCause.OUTGOING_CANCELED);
+ }
+
+ @SmallTest
+ public void testTelCallWithBadCallAction() {
+ badCallActionHelper(Uri.parse("tel:6505551234"), DisconnectCause.INVALID_NUMBER);
+ }
+
+ @SmallTest
+ public void testSipCallWithBadCallAction() {
+ badCallActionHelper(Uri.parse("sip:testuser@testsite.com"), DisconnectCause.INVALID_NUMBER);
+ }
+
+ private void badCallActionHelper(Uri handle, int expectedCode) {
+ Intent intent = new Intent(Intent.ACTION_ALARM_CHANGED, handle);
+
+ int result = processIntent(intent, true);
+
+ assertEquals(expectedCode, result);
+ verifyNoBroadcastSent();
+ verifyNoCallPlaced();
+ }
+
+ @SmallTest
+ public void testNoNumberSupplied() {
+ Uri handle = Uri.parse("tel:");
+ Intent intent = new Intent(Intent.ACTION_CALL, handle);
+
+ int result = processIntent(intent, true);
+
+ assertEquals(DisconnectCause.NO_PHONE_NUMBER_SUPPLIED, result);
+ verifyNoBroadcastSent();
+ verifyNoCallPlaced();
+ }
+
+ @SmallTest
+ public void testEmergencyCallWithNonDefaultDialer() {
+ Uri handle = Uri.parse("tel:6505551911");
+ doReturn(true).when(mPhoneNumberUtilsAdapterSpy).isPotentialLocalEmergencyNumber(
+ any(Context.class), eq(handle.getSchemeSpecificPart()));
+ 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(R.string.ui_default_package, ui_package_string);
+ mComponentContextFixture.putResource(R.string.dialer_default_class,
+ dialer_default_class_string);
+
+ int result = processIntent(intent, false);
+
+ 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());
+ }
+
+ @SmallTest
+ public void testActionCallEmergencyCall() {
+ Uri handle = Uri.parse("tel:6505551911");
+ Intent intent = buildIntent(handle, Intent.ACTION_CALL, null);
+ emergencyCallTestHelper(intent, null);
+ }
+
+ @SmallTest
+ public void testActionEmergencyWithEmergencyNumber() {
+ Uri handle = Uri.parse("tel:6505551911");
+ Intent intent = buildIntent(handle, Intent.ACTION_CALL_EMERGENCY, null);
+ emergencyCallTestHelper(intent, null);
+ }
+
+ @SmallTest
+ public void testActionPrivCallWithEmergencyNumber() {
+ Uri handle = Uri.parse("tel:6505551911");
+ Intent intent = buildIntent(handle, Intent.ACTION_CALL_PRIVILEGED, null);
+ emergencyCallTestHelper(intent, null);
+ }
+
+ @SmallTest
+ public void testEmergencyCallWithGatewayExtras() {
+ Uri handle = Uri.parse("tel:6505551911");
+ Bundle gatewayExtras = new Bundle();
+ gatewayExtras.putString(NewOutgoingCallIntentBroadcaster.EXTRA_GATEWAY_PROVIDER_PACKAGE,
+ "sample1");
+ gatewayExtras.putString(NewOutgoingCallIntentBroadcaster.EXTRA_GATEWAY_URI, "sample2");
+
+ Intent intent = buildIntent(handle, Intent.ACTION_CALL, gatewayExtras);
+ emergencyCallTestHelper(intent, gatewayExtras);
+ }
+
+ @SmallTest
+ public void testActionEmergencyWithNonEmergencyNumber() {
+ Uri handle = Uri.parse("tel:6505551911");
+ doReturn(false).when(mPhoneNumberUtilsAdapterSpy).isPotentialLocalEmergencyNumber(
+ any(Context.class), eq(handle.getSchemeSpecificPart()));
+ Intent intent = new Intent(Intent.ACTION_CALL_EMERGENCY, handle);
+ int result = processIntent(intent, true);
+
+ assertEquals(DisconnectCause.OUTGOING_CANCELED, result);
+ verifyNoCallPlaced();
+ verifyNoBroadcastSent();
+ }
+
+ private void emergencyCallTestHelper(Intent intent, Bundle expectedAdditionalExtras) {
+ Uri handle = intent.getData();
+ int videoState = VideoProfile.STATE_BIDIRECTIONAL;
+ boolean isSpeakerphoneOn = true;
+ doReturn(true).when(mPhoneNumberUtilsAdapterSpy).isPotentialLocalEmergencyNumber(
+ any(Context.class), eq(handle.getSchemeSpecificPart()));
+ intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isSpeakerphoneOn);
+ intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
+ int result = processIntent(intent, true);
+
+ assertEquals(DisconnectCause.NOT_DISCONNECTED, result);
+ verify(mCallsManager).placeOutgoingCall(eq(mCall), eq(handle), isNull(GatewayInfo.class),
+ eq(isSpeakerphoneOn), eq(videoState));
+
+ Bundle expectedExtras = createNumberExtras(handle.getSchemeSpecificPart());
+ if (expectedAdditionalExtras != null) {
+ expectedExtras.putAll(expectedAdditionalExtras);
+ }
+ BroadcastReceiver receiver = verifyBroadcastSent(handle.getSchemeSpecificPart(),
+ expectedExtras).receiver;
+ assertNull(receiver);
+ }
+
+ @SmallTest
+ public void testUnmodifiedRegularCall() {
+ Uri handle = Uri.parse("tel:6505551234");
+ Intent callIntent = buildIntent(handle, Intent.ACTION_CALL, null);
+ ReceiverIntentPair result = regularCallTestHelper(callIntent, null);
+
+ result.receiver.setResultData(
+ result.intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+
+ result.receiver.onReceive(mContext, result.intent);
+
+ verify(mCallsManager).placeOutgoingCall(eq(mCall), eq(handle), isNull(GatewayInfo.class),
+ eq(true), eq(VideoProfile.STATE_BIDIRECTIONAL));
+ }
+
+ @SmallTest
+ public void testUnmodifiedSipCall() {
+ Uri handle = Uri.parse("sip:test@test.com");
+ Intent callIntent = buildIntent(handle, Intent.ACTION_CALL, null);
+ ReceiverIntentPair result = regularCallTestHelper(callIntent, null);
+
+ result.receiver.setResultData(
+ result.intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+
+ result.receiver.onReceive(mContext, result.intent);
+
+ Uri encHandle = Uri.fromParts(handle.getScheme(),
+ handle.getSchemeSpecificPart(), null);
+ verify(mCallsManager).placeOutgoingCall(eq(mCall), eq(encHandle), isNull(GatewayInfo.class),
+ eq(true), eq(VideoProfile.STATE_BIDIRECTIONAL));
+ }
+
+ @SmallTest
+ public void testCallWithGatewayInfo() {
+ Uri handle = Uri.parse("tel:6505551234");
+ Intent callIntent = buildIntent(handle, Intent.ACTION_CALL, null);
+
+ callIntent.putExtra(NewOutgoingCallIntentBroadcaster
+ .EXTRA_GATEWAY_PROVIDER_PACKAGE, "sample1");
+ callIntent.putExtra(NewOutgoingCallIntentBroadcaster.EXTRA_GATEWAY_URI, "sample2");
+ ReceiverIntentPair result = regularCallTestHelper(callIntent, callIntent.getExtras());
+
+ result.receiver.setResultData(
+ result.intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+
+ result.receiver.onReceive(mContext, result.intent);
+
+ verify(mCallsManager).placeOutgoingCall(eq(mCall), eq(handle),
+ isNotNull(GatewayInfo.class), eq(true), eq(VideoProfile.STATE_BIDIRECTIONAL));
+ }
+
+ @SmallTest
+ public void testCallNumberModifiedToNull() {
+ Uri handle = Uri.parse("tel:6505551234");
+ Intent callIntent = buildIntent(handle, Intent.ACTION_CALL, null);
+ ReceiverIntentPair result = regularCallTestHelper(callIntent, null);
+
+ result.receiver.setResultData(null);
+
+ result.receiver.onReceive(mContext, result.intent);
+ verifyNoCallPlaced();
+ verify(mCall).disconnect(true);
+ }
+
+ @SmallTest
+ public void testCallModifiedToEmergency() {
+ Uri handle = Uri.parse("tel:6505551234");
+ Intent callIntent = buildIntent(handle, Intent.ACTION_CALL, null);
+ ReceiverIntentPair result = regularCallTestHelper(callIntent, null);
+
+ String newEmergencyNumber = "1234567890";
+ result.receiver.setResultData(newEmergencyNumber);
+
+ doReturn(true).when(mPhoneNumberUtilsAdapterSpy).isPotentialLocalEmergencyNumber(
+ any(Context.class), eq(newEmergencyNumber));
+ result.receiver.onReceive(mContext, result.intent);
+ verify(mCall).disconnect(true);
+ }
+
+ private ReceiverIntentPair regularCallTestHelper(Intent intent,
+ Bundle expectedAdditionalExtras) {
+ Uri handle = intent.getData();
+ int videoState = VideoProfile.STATE_BIDIRECTIONAL;
+ boolean isSpeakerphoneOn = true;
+ intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isSpeakerphoneOn);
+ intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
+
+ int result = processIntent(intent, true);
+
+ assertEquals(DisconnectCause.NOT_DISCONNECTED, result);
+ Bundle expectedExtras = createNumberExtras(handle.getSchemeSpecificPart());
+ if (expectedAdditionalExtras != null) {
+ expectedExtras.putAll(expectedAdditionalExtras);
+ }
+ return verifyBroadcastSent(handle.getSchemeSpecificPart(), expectedExtras);
+ }
+
+ private Intent buildIntent(Uri handle, String action, Bundle extras) {
+ Intent i = new Intent(action, handle);
+ if (extras != null) {
+ i.putExtras(extras);
+ }
+ return i;
+ }
+
+ private int processIntent(Intent intent,
+ boolean isDefaultPhoneApp) {
+ NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster(
+ mContext, mCallsManager, mCall, intent, mPhoneNumberUtilsAdapterSpy,
+ isDefaultPhoneApp);
+ return b.processIntent();
+ }
+
+ private ReceiverIntentPair verifyBroadcastSent(String number, Bundle expectedExtras) {
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+ verify(mContext).sendOrderedBroadcastAsUser(
+ intentCaptor.capture(),
+ eq(UserHandle.CURRENT),
+ eq(Manifest.permission.PROCESS_OUTGOING_CALLS),
+ eq(AppOpsManager.OP_PROCESS_OUTGOING_CALLS),
+ receiverCaptor.capture(),
+ isNull(Handler.class),
+ eq(Activity.RESULT_OK),
+ eq(number),
+ isNull(Bundle.class));
+
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(Intent.ACTION_NEW_OUTGOING_CALL, capturedIntent.getAction());
+ assertEquals(Intent.FLAG_RECEIVER_FOREGROUND, capturedIntent.getFlags());
+ assertTrue(areBundlesEqual(expectedExtras, capturedIntent.getExtras()));
+
+ BroadcastReceiver receiver = receiverCaptor.getValue();
+ if (receiver != null) {
+ receiver.setPendingResult(
+ new BroadcastReceiver.PendingResult(0, "", null, 0, true, false, null, 0, 0));
+ }
+
+ return new ReceiverIntentPair(receiver, capturedIntent);
+ }
+
+ private Bundle createNumberExtras(String number) {
+ Bundle b = new Bundle();
+ b.putString(Intent.EXTRA_PHONE_NUMBER, number);
+ return b;
+ }
+
+ private void verifyNoCallPlaced() {
+ verify(mCallsManager, never()).placeOutgoingCall(any(Call.class), any(Uri.class),
+ any(GatewayInfo.class), anyBoolean(), anyInt());
+ }
+
+ private void verifyNoBroadcastSent() {
+ verify(mContext, never()).sendOrderedBroadcastAsUser(
+ any(Intent.class),
+ any(UserHandle.class),
+ anyString(),
+ anyInt(),
+ any(BroadcastReceiver.class),
+ any(Handler.class),
+ anyInt(),
+ anyString(),
+ any(Bundle.class));
+ }
+
+ private static boolean areBundlesEqual(Bundle b1, Bundle b2) {
+ for (String key1 : b1.keySet()) {
+ if (!b1.get(key1).equals(b2.get(key1))) {
+ return false;
+ }
+ }
+
+ for (String key2 : b2.keySet()) {
+ if (!b2.get(key2).equals(b1.get(key2))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index f815fed..67e3f70 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -16,44 +16,54 @@
package com.android.server.telecom.tests;
-import android.os.Binder;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Xml;
import com.android.internal.telecom.IConnectionService;
import com.android.internal.util.FastXmlSerializer;
import com.android.server.telecom.Log;
import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.PhoneAccountRegistrar.DefaultPhoneAccountHandle;
+import org.mockito.Mock;
import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
-import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.os.Parcel;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
-import android.util.Xml;
-
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Arrays;
+import java.util.Set;
public class PhoneAccountRegistrarTest extends TelecomTestCase {
private static final int MAX_VERSION = Integer.MAX_VALUE;
private static final String FILE_NAME = "phone-account-registrar-test-1223.xml";
private PhoneAccountRegistrar mRegistrar;
+ @Mock
+ private TelecomManager mTelecomManager;
@Override
public void setUp() throws Exception {
super.setUp();
- mComponentContextFixture = new ComponentContextFixture();
+ MockitoAnnotations.initMocks(this);
+ mComponentContextFixture.setTelecomManager(mTelecomManager);
new File(
mComponentContextFixture.getTestDouble().getApplicationContext().getFilesDir(),
FILE_NAME)
@@ -70,7 +80,6 @@
mComponentContextFixture.getTestDouble().getApplicationContext().getFilesDir(),
FILE_NAME)
.delete();
- mComponentContextFixture = null;
super.tearDown();
}
@@ -88,9 +97,19 @@
}
public void testPhoneAccount() throws Exception {
+ Bundle testBundle = new Bundle();
+ testBundle.putInt("EXTRA_INT_1", 1);
+ testBundle.putInt("EXTRA_INT_100", 100);
+ testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+ testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+ testBundle.putString("EXTRA_STR1", "Hello");
+ testBundle.putString("EXTRA_STR2", "There");
+
PhoneAccount input = makeQuickAccountBuilder("id0", 0)
.addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
.addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+ .setExtras(testBundle)
+ .setIsEnabled(true)
.build();
PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
mContext);
@@ -98,6 +117,38 @@
assertPhoneAccountEquals(input, result);
}
+ /**
+ * Test to ensure non-supported balues
+ * @throws Exception
+ */
+ public void testPhoneAccountExtrasEdge() throws Exception {
+ Bundle testBundle = new Bundle();
+ // Ensure null values for string are not persisted.
+ testBundle.putString("EXTRA_STR2", null);
+ //
+
+ // Ensure unsupported data types are not persisted.
+ testBundle.putShort("EXTRA_SHORT", (short) 2);
+ testBundle.putByte("EXTRA_BYTE", (byte) 1);
+ testBundle.putParcelable("EXTRA_PARC", new Rect(1, 1, 1, 1));
+ // Put in something valid so the bundle exists.
+ testBundle.putString("EXTRA_OK", "OK");
+
+ PhoneAccount input = makeQuickAccountBuilder("id0", 0)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+ .setExtras(testBundle)
+ .build();
+ PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
+ mContext);
+
+ Bundle extras = result.getExtras();
+ assertFalse(extras.keySet().contains("EXTRA_STR2"));
+ assertFalse(extras.keySet().contains("EXTRA_SHORT"));
+ assertFalse(extras.keySet().contains("EXTRA_BYTE"));
+ assertFalse(extras.keySet().contains("EXTRA_PARC"));
+ }
+
public void testState() throws Exception {
PhoneAccountRegistrar.State input = makeQuickState();
PhoneAccountRegistrar.State result = roundTripXml(this, input,
@@ -134,10 +185,11 @@
.setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
.build());
- assertEquals(4, mRegistrar.getAllPhoneAccountHandles().size());
- assertEquals(3, mRegistrar.getCallCapablePhoneAccounts(null, false).size());
- assertEquals(null, mRegistrar.getSimCallManager());
- assertEquals(null, mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_TEL));
+ assertEquals(4, mRegistrar.getAllPhoneAccountsOfCurrentUser().size());
+ assertEquals(3, mRegistrar.getCallCapablePhoneAccountsOfCurrentUser(null, false).size());
+ assertEquals(null, mRegistrar.getSimCallManagerOfCurrentUser());
+ assertEquals(null, mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ PhoneAccount.SCHEME_TEL));
}
public void testSimCallManager() throws Exception {
@@ -150,7 +202,8 @@
Mockito.mock(IConnectionService.class));
// By default, there is no default outgoing account (nothing has been registered)
- assertNull(mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_TEL));
+ assertNull(
+ mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL));
// Register one tel: account
PhoneAccountHandle telAccount = makeQuickAccountHandle("tel_acct");
@@ -159,7 +212,7 @@
.addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
.build());
PhoneAccountHandle defaultAccount =
- mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_TEL);
+ mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL);
assertEquals(telAccount, defaultAccount);
// Add a SIP account, make sure tel: doesn't change
@@ -168,9 +221,11 @@
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.addSupportedUriScheme(PhoneAccount.SCHEME_SIP)
.build());
- defaultAccount = mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_SIP);
+ defaultAccount = mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ PhoneAccount.SCHEME_SIP);
assertEquals(sipAccount, defaultAccount);
- defaultAccount = mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_TEL);
+ defaultAccount = mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ PhoneAccount.SCHEME_TEL);
assertEquals(telAccount, defaultAccount);
// Add a connection manager, make sure tel: doesn't change
@@ -179,12 +234,14 @@
.setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
.addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
.build());
- defaultAccount = mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_TEL);
+ defaultAccount = mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+ PhoneAccount.SCHEME_TEL);
assertEquals(telAccount, defaultAccount);
// Unregister the tel: account, make sure there is no tel: default now.
mRegistrar.unregisterPhoneAccount(telAccount);
- assertNull(mRegistrar.getOutgoingPhoneAccountForScheme(PhoneAccount.SCHEME_TEL));
+ assertNull(
+ mRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(PhoneAccount.SCHEME_TEL));
}
public void testPhoneAccountParceling() throws Exception {
@@ -228,7 +285,7 @@
return new PhoneAccountHandle(
makeQuickConnectionServiceComponentName(),
id,
- Binder.getCallingUserHandle());
+ Process.myUserHandle());
}
private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
@@ -318,6 +375,18 @@
}
}
+ private static void assertDefaultPhoneAccountHandleEquals(DefaultPhoneAccountHandle a,
+ DefaultPhoneAccountHandle b) {
+ if (a != b) {
+ if (a!= null && b != null) {
+ assertEquals(a.userHandle, b.userHandle);
+ assertPhoneAccountHandleEquals(a.phoneAccountHandle, b.phoneAccountHandle);
+ } else {
+ fail("Default phone account handles are not equal: " + a + ", " + b);
+ }
+ }
+ }
+
private static void assertPhoneAccountEquals(PhoneAccount a, PhoneAccount b) {
if (a != b) {
if (a != null && b != null) {
@@ -330,15 +399,40 @@
assertEquals(a.getLabel(), b.getLabel());
assertEquals(a.getShortDescription(), b.getShortDescription());
assertEquals(a.getSupportedUriSchemes(), b.getSupportedUriSchemes());
+ assertBundlesEqual(a.getExtras(), b.getExtras());
+ assertEquals(a.isEnabled(), b.isEnabled());
} else {
fail("Phone accounts not equal: " + a + ", " + b);
}
}
}
+ private static void assertBundlesEqual(Bundle a, Bundle b) {
+ if (a == null && b == null) {
+ return;
+ }
+
+ assertNotNull(a);
+ assertNotNull(b);
+ Set<String> keySetA = a.keySet();
+ Set<String> keySetB = b.keySet();
+
+ assertTrue("Bundle keys not the same", keySetA.containsAll(keySetB));
+ assertTrue("Bundle keys not the same", keySetB.containsAll(keySetA));
+
+ for (String keyA : keySetA) {
+ assertEquals("Bundle value not the same", a.get(keyA), b.get(keyA));
+ }
+ }
+
private static void assertStateEquals(
PhoneAccountRegistrar.State a, PhoneAccountRegistrar.State b) {
- assertPhoneAccountHandleEquals(a.defaultOutgoing, b.defaultOutgoing);
+ assertEquals(a.defaultOutgoingAccountHandles.size(),
+ b.defaultOutgoingAccountHandles.size());
+ for (int i = 0; i < a.defaultOutgoingAccountHandles.size(); i++) {
+ assertDefaultPhoneAccountHandleEquals(a.defaultOutgoingAccountHandles.get(i),
+ b.defaultOutgoingAccountHandles.get(i));
+ }
assertEquals(a.accounts.size(), b.accounts.size());
for (int i = 0; i < a.accounts.size(); i++) {
assertPhoneAccountEquals(a.accounts.get(i), b.accounts.get(i));
@@ -350,7 +444,11 @@
s.accounts.add(makeQuickAccount("id0", 0));
s.accounts.add(makeQuickAccount("id1", 1));
s.accounts.add(makeQuickAccount("id2", 2));
- s.defaultOutgoing = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id0");
+ PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(
+ new ComponentName("pkg0", "cls0"), "id0");
+ UserHandle userHandle = phoneAccountHandle.getUserHandle();
+ s.defaultOutgoingAccountHandles
+ .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle));
return s;
}
}
diff --git a/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
new file mode 100644
index 0000000..f08da0c
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.PowerManager;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ProximitySensorManager;
+import com.android.server.telecom.TelecomWakeLock;
+
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ProximitySensorManagerTest extends TelecomTestCase{
+
+ @Mock CallsManager mCallsManager;
+ @Mock Call mCall;
+ @Mock TelecomWakeLock.WakeLockAdapter mWakeLockAdapter;
+ private ProximitySensorManager mProximitySensorManager;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ TelecomWakeLock telecomWakeLock = new TelecomWakeLock(
+ null, // Context is never used due to mock WakeLockAdapter
+ mWakeLockAdapter, PowerManager.FULL_WAKE_LOCK,
+ InCallWakeLockControllerTest.class.getSimpleName());
+ mProximitySensorManager = new ProximitySensorManager(telecomWakeLock, mCallsManager);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mProximitySensorManager = null;
+ super.tearDown();
+ }
+
+ public void testTurnOnProximityWithCallsActive() throws Exception {
+ when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>(){{
+ add(mCall);
+ }});
+ when(mWakeLockAdapter.isHeld()).thenReturn(false);
+
+ mProximitySensorManager.turnOn();
+
+ verify(mWakeLockAdapter).acquire();
+ }
+
+ public void testTurnOnProximityWithNoCallsActive() throws Exception {
+ when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>());
+ when(mWakeLockAdapter.isHeld()).thenReturn(false);
+
+ mProximitySensorManager.turnOn();
+
+ verify(mWakeLockAdapter, never()).acquire();
+
+ }
+
+ public void testTurnOffProximityExplicitly() throws Exception {
+ when(mWakeLockAdapter.isHeld()).thenReturn(true);
+
+ mProximitySensorManager.turnOff(true);
+
+ verify(mWakeLockAdapter).release(0);
+ }
+
+ public void testCallRemovedFromCallsManagerCallsActive() throws Exception {
+ when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>(){{
+ add(mCall);
+ }});
+ when(mWakeLockAdapter.isHeld()).thenReturn(true);
+
+ mProximitySensorManager.onCallRemoved(mock(Call.class));
+
+ verify(mWakeLockAdapter, never()).release(0);
+ }
+
+ public void testCallRemovedFromCallsManagerNoCallsActive() throws Exception {
+ when(mCallsManager.getCalls()).thenReturn(new ArrayList<Call>());
+ when(mWakeLockAdapter.isHeld()).thenReturn(true);
+
+ mProximitySensorManager.onCallRemoved(mock(Call.class));
+
+ verify(mWakeLockAdapter).release(0);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/StateMachineTestBase.java b/tests/src/com/android/server/telecom/tests/StateMachineTestBase.java
new file mode 100644
index 0000000..358b960
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/StateMachineTestBase.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.internal.util.StateMachine;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+public abstract class StateMachineTestBase<T extends StateMachine> extends TelecomTestCase {
+ abstract static class TestParameters {}
+
+ protected final void waitForStateMachineActionCompletion(T stateMachine, int runnableCode) {
+ final CountDownLatch lock = new CountDownLatch(1);
+ Runnable actionComplete = new Runnable() {
+ @Override
+ public void run() {
+ lock.countDown();
+ }
+ };
+ stateMachine.sendMessage(runnableCode, actionComplete);
+ while (lock.getCount() > 0) {
+ try {
+ lock.await();
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ }
+
+ protected final void parametrizedTestStateMachine(
+ List<? extends TestParameters> paramList) throws Throwable {
+ for (TestParameters params : paramList) {
+ try {
+ runParametrizedTestCase(params);
+ } catch (Throwable e) {
+ String newMessage = "Failed at parameters: \n" + params.toString() + '\n'
+ + e.getMessage();
+ Throwable t = new Throwable(newMessage, e);
+ t.setStackTrace(e.getStackTrace());
+ throw t;
+ }
+ }
+ }
+
+ protected abstract void runParametrizedTestCase(TestParameters params) throws Throwable;
+}
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java b/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java
new file mode 100644
index 0000000..02e7ecf
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+
+import com.android.server.telecom.SystemStateProvider;
+import com.android.server.telecom.SystemStateProvider.SystemStateListener;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for SystemStateProvider
+ */
+public class SystemStateProviderTest extends TelecomTestCase {
+
+ SystemStateProvider mSystemStateProvider;
+
+ @Mock Context mContext;
+ @Mock SystemStateListener mSystemStateListener;
+ @Mock UiModeManager mUiModeManager;
+ @Mock Intent mIntentEnter;
+ @Mock Intent mIntentExit;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testListeners() throws Exception {
+ SystemStateProvider systemStateProvider = new SystemStateProvider(mContext);
+
+ assertFalse(systemStateProvider.removeListener(mSystemStateListener));
+ systemStateProvider.addListener(mSystemStateListener);
+ assertTrue(systemStateProvider.removeListener(mSystemStateListener));
+ assertFalse(systemStateProvider.removeListener(mSystemStateListener));
+ }
+
+ public void testQuerySystemForCarMode_True() {
+ when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
+ when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ assertTrue(new SystemStateProvider(mContext).isCarMode());
+ }
+
+ public void testQuerySystemForCarMode_False() {
+ when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
+ when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL);
+ assertFalse(new SystemStateProvider(mContext).isCarMode());
+ }
+
+ public void testReceiverAndIntentFilter() {
+ ArgumentCaptor<IntentFilter> intentFilter = ArgumentCaptor.forClass(IntentFilter.class);
+ new SystemStateProvider(mContext);
+ verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture());
+
+ assertEquals(2, intentFilter.getValue().countActions());
+ assertEquals(UiModeManager.ACTION_ENTER_CAR_MODE, intentFilter.getValue().getAction(0));
+ assertEquals(UiModeManager.ACTION_EXIT_CAR_MODE, intentFilter.getValue().getAction(1));
+ }
+
+ public void testOnEnterExitCarMode() {
+ ArgumentCaptor<BroadcastReceiver> receiver =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ new SystemStateProvider(mContext).addListener(mSystemStateListener);
+
+ verify(mContext).registerReceiver(receiver.capture(), any(IntentFilter.class));
+
+ when(mIntentEnter.getAction()).thenReturn(UiModeManager.ACTION_ENTER_CAR_MODE);
+ receiver.getValue().onReceive(mContext, mIntentEnter);
+ verify(mSystemStateListener).onCarModeChanged(true);
+
+ when(mIntentExit.getAction()).thenReturn(UiModeManager.ACTION_EXIT_CAR_MODE);
+ receiver.getValue().onReceive(mContext, mIntentExit);
+ verify(mSystemStateListener).onCarModeChanged(false);
+
+ receiver.getValue().onReceive(mContext, new Intent("invalid action"));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
new file mode 100644
index 0000000..d0b723a
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -0,0 +1,935 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.CALL_PHONE;
+import static android.Manifest.permission.MODIFY_PHONE_STATE;
+import static android.Manifest.permission.READ_PHONE_STATE;
+import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
+
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telecom.ITelecomService;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIntentProcessor;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomServiceImpl;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TelecomServiceImplTest extends TelecomTestCase {
+ public static class CallIntentProcessAdapterFake implements CallIntentProcessor.Adapter {
+ @Override
+ public void processOutgoingCallIntent(Context context, CallsManager callsManager,
+ Intent intent) {
+
+ }
+
+ @Override
+ public void processIncomingCallIntent(CallsManager callsManager, Intent intent) {
+
+ }
+
+ @Override
+ public void processUnknownCallIntent(CallsManager callsManager, Intent intent) {
+
+ }
+ }
+
+ public static class DefaultDialerManagerAdapterFake
+ implements TelecomServiceImpl.DefaultDialerManagerAdapter {
+ @Override
+ public String getDefaultDialerApplication(Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean setDefaultDialerApplication(Context context, String packageName) {
+ return false;
+ }
+
+ @Override
+ public boolean isDefaultOrSystemDialer(Context context, String packageName) {
+ return false;
+ }
+ }
+
+ public static class SubscriptionManagerAdapterFake
+ implements TelecomServiceImpl.SubscriptionManagerAdapter {
+ @Override
+ public int getDefaultVoiceSubId() {
+ return 0;
+ }
+ }
+
+ private static class AnyStringIn extends ArgumentMatcher<String> {
+ private Collection<String> mStrings;
+ public AnyStringIn(Collection<String> strings) {
+ this.mStrings = strings;
+ }
+
+ @Override
+ public boolean matches(Object string) {
+ return mStrings.contains(string);
+ }
+ }
+
+ private ITelecomService.Stub mTSIBinder;
+ private AppOpsManager mAppOpsManager;
+ private UserManager mUserManager;
+
+ @Mock private CallsManager mFakeCallsManager;
+ @Mock private PhoneAccountRegistrar mFakePhoneAccountRegistrar;
+ @Mock private TelecomManager mTelecomManager;
+ private CallIntentProcessor.Adapter mCallIntentProcessorAdapter =
+ spy(new CallIntentProcessAdapterFake());
+ private TelecomServiceImpl.DefaultDialerManagerAdapter mDefaultDialerManagerAdapter =
+ spy(new DefaultDialerManagerAdapterFake());
+ private TelecomServiceImpl.SubscriptionManagerAdapter mSubscriptionManagerAdapter =
+ spy(new SubscriptionManagerAdapterFake());
+ @Mock private UserCallIntentProcessor mUserCallIntentProcessor;
+
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
+
+ private static final String DEFAULT_DIALER_PACKAGE = "com.google.android.dialer";
+ private static final UserHandle USER_HANDLE_16 = new UserHandle(16);
+ private static final UserHandle USER_HANDLE_17 = new UserHandle(17);
+ private static final PhoneAccountHandle TEL_PA_HANDLE_16 = new PhoneAccountHandle(
+ new ComponentName("test", "telComponentName"), "0", USER_HANDLE_16);
+ private static final PhoneAccountHandle SIP_PA_HANDLE_17 = new PhoneAccountHandle(
+ new ComponentName("test", "sipComponentName"), "1", USER_HANDLE_17);
+ private static final PhoneAccountHandle TEL_PA_HANDLE_CURRENT = new PhoneAccountHandle(
+ new ComponentName("test", "telComponentName"), "2", Binder.getCallingUserHandle());
+ private static final PhoneAccountHandle SIP_PA_HANDLE_CURRENT = new PhoneAccountHandle(
+ new ComponentName("test", "sipComponentName"), "3", Binder.getCallingUserHandle());
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mComponentContextFixture.putBooleanResource(
+ com.android.internal.R.bool.config_voice_capable, true);
+
+ doReturn(mContext).when(mContext).getApplicationContext();
+ doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class),
+ anyString());
+ TelecomServiceImpl telecomServiceImpl = new TelecomServiceImpl(
+ mContext,
+ mFakeCallsManager,
+ mFakePhoneAccountRegistrar,
+ mCallIntentProcessorAdapter,
+ new UserCallIntentProcessorFactory() {
+ @Override
+ public UserCallIntentProcessor create(Context context, UserHandle userHandle) {
+ return mUserCallIntentProcessor;
+ }
+ },
+ mDefaultDialerManagerAdapter,
+ mSubscriptionManagerAdapter,
+ mLock);
+ mTSIBinder = telecomServiceImpl.getBinder();
+ mComponentContextFixture.setTelecomManager(mTelecomManager);
+ when(mTelecomManager.getDefaultDialerPackage()).thenReturn(DEFAULT_DIALER_PACKAGE);
+ when(mTelecomManager.getSystemDialerPackage()).thenReturn(DEFAULT_DIALER_PACKAGE);
+
+ mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+
+ doReturn(DEFAULT_DIALER_PACKAGE)
+ .when(mDefaultDialerManagerAdapter)
+ .getDefaultDialerApplication(any(Context.class));
+
+ doReturn(true)
+ .when(mDefaultDialerManagerAdapter)
+ .isDefaultOrSystemDialer(any(Context.class), eq(DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testGetDefaultOutgoingPhoneAccount() throws RemoteException {
+ when(mFakePhoneAccountRegistrar
+ .getOutgoingPhoneAccountForScheme(eq("tel"), any(UserHandle.class)))
+ .thenReturn(TEL_PA_HANDLE_16);
+ when(mFakePhoneAccountRegistrar
+ .getOutgoingPhoneAccountForScheme(eq("sip"), any(UserHandle.class)))
+ .thenReturn(SIP_PA_HANDLE_17);
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+
+ PhoneAccountHandle returnedHandleTel
+ = mTSIBinder.getDefaultOutgoingPhoneAccount("tel", DEFAULT_DIALER_PACKAGE);
+ assertEquals(TEL_PA_HANDLE_16, returnedHandleTel);
+
+ PhoneAccountHandle returnedHandleSip
+ = mTSIBinder.getDefaultOutgoingPhoneAccount("sip", DEFAULT_DIALER_PACKAGE);
+ assertEquals(SIP_PA_HANDLE_17, returnedHandleSip);
+ }
+
+ @SmallTest
+ public void testGetDefaultOutgoingPhoneAccountFailure() throws RemoteException {
+ // make sure that the list of user profiles doesn't include anything the PhoneAccountHandles
+ // are associated with
+
+ when(mFakePhoneAccountRegistrar
+ .getOutgoingPhoneAccountForScheme(eq("tel"), any(UserHandle.class)))
+ .thenReturn(TEL_PA_HANDLE_16);
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_16)).thenReturn(
+ makePhoneAccount(TEL_PA_HANDLE_16).build());
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_READ_PHONE_STATE), anyInt(), anyString()))
+ .thenReturn(AppOpsManager.MODE_IGNORED);
+ doThrow(new SecurityException()).when(mContext)
+ .enforceCallingOrSelfPermission(eq(READ_PRIVILEGED_PHONE_STATE), anyString());
+
+ PhoneAccountHandle returnedHandleTel
+ = mTSIBinder.getDefaultOutgoingPhoneAccount("tel", "");
+ assertNull(returnedHandleTel);
+ }
+
+ @SmallTest
+ public void testGetUserSelectedOutgoingPhoneAccount() throws RemoteException {
+ when(mFakePhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount(any(UserHandle.class)))
+ .thenReturn(TEL_PA_HANDLE_16);
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_16)).thenReturn(
+ makeMultiUserPhoneAccount(TEL_PA_HANDLE_16).build());
+
+ PhoneAccountHandle returnedHandle
+ = mTSIBinder.getUserSelectedOutgoingPhoneAccount();
+ assertEquals(TEL_PA_HANDLE_16, returnedHandle);
+ }
+
+ @SmallTest
+ public void testSetUserSelectedOutgoingPhoneAccount() throws RemoteException {
+ mTSIBinder.setUserSelectedOutgoingPhoneAccount(TEL_PA_HANDLE_16);
+ verify(mFakePhoneAccountRegistrar)
+ .setUserSelectedOutgoingPhoneAccount(eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+ }
+
+ @SmallTest
+ public void testSetUserSelectedOutgoingPhoneAccountFailure() throws RemoteException {
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ anyString(), anyString());
+ try {
+ mTSIBinder.setUserSelectedOutgoingPhoneAccount(TEL_PA_HANDLE_16);
+ } catch (SecurityException e) {
+ // desired result
+ }
+ verify(mFakePhoneAccountRegistrar, never())
+ .setUserSelectedOutgoingPhoneAccount(
+ any(PhoneAccountHandle.class), any(UserHandle.class));
+ }
+
+ @SmallTest
+ public void testGetCallCapablePhoneAccounts() throws RemoteException {
+ List<PhoneAccountHandle> fullPHList = new ArrayList<PhoneAccountHandle>() {{
+ add(TEL_PA_HANDLE_16);
+ add(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(anyString(), eq(true), any(UserHandle.class)))
+ .thenReturn(fullPHList);
+ // Returns only enabled phone accounts when getCallCapablePhoneAccounts is called.
+ when(mFakePhoneAccountRegistrar
+ .getCallCapablePhoneAccounts(anyString(), eq(false), any(UserHandle.class)))
+ .thenReturn(smallPHList);
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+
+ assertEquals(fullPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(true, DEFAULT_DIALER_PACKAGE));
+ assertEquals(smallPHList,
+ mTSIBinder.getCallCapablePhoneAccounts(false, DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testGetCallCapablePhoneAccountsFailure() throws RemoteException {
+ List<String> enforcedPermissions = new ArrayList<String>() {{
+ add(READ_PHONE_STATE);
+ add(READ_PRIVILEGED_PHONE_STATE);
+ }};
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ argThat(new AnyStringIn(enforcedPermissions)), anyString());
+
+ List<PhoneAccountHandle> result = null;
+ try {
+ result = mTSIBinder.getCallCapablePhoneAccounts(true, "");
+ } catch (SecurityException e) {
+ // intended behavior
+ }
+ assertNull(result);
+ verify(mFakePhoneAccountRegistrar, never())
+ .getCallCapablePhoneAccounts(anyString(), anyBoolean(), any(UserHandle.class));
+ }
+
+ @SmallTest
+ public void testGetPhoneAccountsSupportingScheme() throws RemoteException {
+ List<PhoneAccountHandle> sipPHList = new ArrayList<PhoneAccountHandle>() {{
+ add(SIP_PA_HANDLE_17);
+ }};
+
+ List<PhoneAccountHandle> telPHList = new ArrayList<PhoneAccountHandle>() {{
+ add(TEL_PA_HANDLE_16);
+ }};
+ when(mFakePhoneAccountRegistrar
+ .getCallCapablePhoneAccounts(eq("tel"), anyBoolean(), any(UserHandle.class)))
+ .thenReturn(telPHList);
+ when(mFakePhoneAccountRegistrar
+ .getCallCapablePhoneAccounts(eq("sip"), anyBoolean(), any(UserHandle.class)))
+ .thenReturn(sipPHList);
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+
+ assertEquals(telPHList,
+ mTSIBinder.getPhoneAccountsSupportingScheme("tel", DEFAULT_DIALER_PACKAGE));
+ assertEquals(sipPHList,
+ mTSIBinder.getPhoneAccountsSupportingScheme("sip", DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testGetPhoneAccountsForPackage() throws RemoteException {
+ List<PhoneAccountHandle> phoneAccountHandleList = new ArrayList<PhoneAccountHandle>() {{
+ add(TEL_PA_HANDLE_16);
+ add(SIP_PA_HANDLE_17);
+ }};
+ when(mFakePhoneAccountRegistrar
+ .getPhoneAccountsForPackage(anyString(), any(UserHandle.class)))
+ .thenReturn(phoneAccountHandleList);
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+ assertEquals(phoneAccountHandleList,
+ mTSIBinder.getPhoneAccountsForPackage(
+ TEL_PA_HANDLE_16.getComponentName().getPackageName()));
+ }
+
+ @SmallTest
+ public void testGetPhoneAccount() throws RemoteException {
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+ assertEquals(TEL_PA_HANDLE_16, mTSIBinder.getPhoneAccount(TEL_PA_HANDLE_16)
+ .getAccountHandle());
+ assertEquals(SIP_PA_HANDLE_17, mTSIBinder.getPhoneAccount(SIP_PA_HANDLE_17)
+ .getAccountHandle());
+ }
+
+ @SmallTest
+ 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)))
+ .thenReturn(phoneAccountList);
+
+ assertEquals(2, mTSIBinder.getAllPhoneAccounts().size());
+ }
+
+ @SmallTest
+ public void testRegisterPhoneAccount() throws RemoteException {
+ String packageNameToUse = "com.android.officialpackage";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "test", Binder.getCallingUserHandle());
+ PhoneAccount phoneAccount = makePhoneAccount(phHandle).build();
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+
+ registerPhoneAccountTestHelper(phoneAccount, true);
+ }
+
+ @SmallTest
+ 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
+ String packageNameToUse = "com.thirdparty.connectionservice";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "asdf", Binder.getCallingUserHandle());
+ PhoneAccount phoneAccount = makePhoneAccount(phHandle).build();
+
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+ PackageManager pm = mContext.getPackageManager();
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)).thenReturn(true);
+
+ registerPhoneAccountTestHelper(phoneAccount, true);
+ }
+
+ @SmallTest
+ public void testRegisterPhoneAccountWithoutModifyPermissionFailure() throws RemoteException {
+ // tests the case where the third party package should not be allowed to register a phone
+ // account due to the lack of modify permission.
+ String packageNameToUse = "com.thirdparty.connectionservice";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "asdf", Binder.getCallingUserHandle());
+ PhoneAccount phoneAccount = makePhoneAccount(phHandle).build();
+
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+ PackageManager pm = mContext.getPackageManager();
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)).thenReturn(false);
+
+ registerPhoneAccountTestHelper(phoneAccount, false);
+ }
+
+ @SmallTest
+ public void testRegisterPhoneAccountWithoutSimSubscriptionPermissionFailure()
+ throws RemoteException {
+ String packageNameToUse = "com.thirdparty.connectionservice";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "asdf", Binder.getCallingUserHandle());
+ PhoneAccount phoneAccount = makePhoneAccount(phHandle)
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION).build();
+
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+ doThrow(new SecurityException())
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(REGISTER_SIM_SUBSCRIPTION), anyString());
+
+ registerPhoneAccountTestHelper(phoneAccount, false);
+ }
+
+ @SmallTest
+ public void testRegisterPhoneAccountWithoutMultiUserPermissionFailure()
+ throws Exception {
+ String packageNameToUse = "com.thirdparty.connectionservice";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "asdf", Binder.getCallingUserHandle());
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(phHandle).build();
+
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+
+ PackageManager packageManager = mContext.getPackageManager();
+ when(packageManager.getApplicationInfo(packageNameToUse, PackageManager.GET_META_DATA))
+ .thenReturn(new ApplicationInfo());
+
+ registerPhoneAccountTestHelper(phoneAccount, false);
+ }
+
+ private void registerPhoneAccountTestHelper(PhoneAccount testPhoneAccount,
+ boolean shouldSucceed) throws RemoteException {
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ boolean didExceptionOccur = false;
+ try {
+ mTSIBinder.registerPhoneAccount(testPhoneAccount);
+ } catch (Exception e) {
+ didExceptionOccur = true;
+ }
+
+ if (shouldSucceed) {
+ assertFalse(didExceptionOccur);
+ verify(mFakePhoneAccountRegistrar).registerPhoneAccount(testPhoneAccount);
+ verify(mContext).sendBroadcastAsUser(intentCaptor.capture(), eq(UserHandle.ALL),
+ anyString());
+
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED,
+ capturedIntent.getAction());
+ Bundle intentExtras = capturedIntent.getExtras();
+ assertEquals(1, intentExtras.size());
+ assertEquals(testPhoneAccount.getAccountHandle(),
+ intentExtras.get(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
+ } else {
+ assertTrue(didExceptionOccur);
+ verify(mFakePhoneAccountRegistrar, never())
+ .registerPhoneAccount(any(PhoneAccount.class));
+ verify(mContext, never())
+ .sendBroadcastAsUser(any(Intent.class), any(UserHandle.class), anyString());
+ }
+ }
+
+ @SmallTest
+ public void testUnregisterPhoneAccount() throws RemoteException {
+ String packageNameToUse = "com.android.officialpackage";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "test", Binder.getCallingUserHandle());
+
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+
+ mTSIBinder.unregisterPhoneAccount(phHandle);
+ verify(mFakePhoneAccountRegistrar).unregisterPhoneAccount(phHandle);
+ verify(mContext).sendBroadcastAsUser(intentCaptor.capture(), eq(UserHandle.ALL),
+ anyString());
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED,
+ capturedIntent.getAction());
+ Bundle intentExtras = capturedIntent.getExtras();
+ assertEquals(1, intentExtras.size());
+ assertEquals(phHandle, intentExtras.get(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
+ }
+
+ @SmallTest
+ public void testUnregisterPhoneAccountFailure() throws RemoteException {
+ String packageNameToUse = "com.thirdparty.connectionservice";
+ PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+ packageNameToUse, "cs"), "asdf", Binder.getCallingUserHandle());
+
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+ PackageManager pm = mContext.getPackageManager();
+ when(pm.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)).thenReturn(false);
+
+ try {
+ mTSIBinder.unregisterPhoneAccount(phHandle);
+ } catch (UnsupportedOperationException e) {
+ // expected behavior
+ }
+ verify(mFakePhoneAccountRegistrar, never())
+ .unregisterPhoneAccount(any(PhoneAccountHandle.class));
+ verify(mContext, never())
+ .sendBroadcastAsUser(any(Intent.class), any(UserHandle.class), anyString());
+ }
+
+ @SmallTest
+ public void testAddNewIncomingCall() throws Exception {
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ Bundle extras = createSampleExtras();
+
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_CURRENT, extras);
+
+ addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
+ CallIntentProcessor.KEY_IS_INCOMING_CALL, extras, false);
+ }
+
+ @SmallTest
+ public void testAddNewIncomingCallFailure() throws Exception {
+ try {
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null);
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ doThrow(new SecurityException()).when(mAppOpsManager).checkPackage(anyInt(), anyString());
+
+ try {
+ mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_CURRENT, null);
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ // Verify that neither of these attempts got through
+ verify(mCallIntentProcessorAdapter, never())
+ .processIncomingCallIntent(any(CallsManager.class), any(Intent.class));
+ }
+
+ @SmallTest
+ public void testAddNewUnknownCall() throws Exception {
+ doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+ Bundle extras = createSampleExtras();
+
+ mTSIBinder.addNewUnknownCall(TEL_PA_HANDLE_CURRENT, extras);
+
+ addCallTestHelper(TelecomManager.ACTION_NEW_UNKNOWN_CALL,
+ CallIntentProcessor.KEY_IS_UNKNOWN_CALL, extras, true);
+ }
+
+ @SmallTest
+ public void testAddNewUnknownCallFailure() throws Exception {
+ try {
+ mTSIBinder.addNewUnknownCall(TEL_PA_HANDLE_16, null);
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ doThrow(new SecurityException()).when(mAppOpsManager).checkPackage(anyInt(), anyString());
+
+ try {
+ mTSIBinder.addNewUnknownCall(TEL_PA_HANDLE_CURRENT, null);
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ // Verify that neither of these attempts got through
+ verify(mCallIntentProcessorAdapter, never())
+ .processIncomingCallIntent(any(CallsManager.class), any(Intent.class));
+ }
+
+ private void addCallTestHelper(String expectedAction, String extraCallKey,
+ Bundle expectedExtras, boolean isUnknown) {
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ if (isUnknown) {
+ verify(mCallIntentProcessorAdapter).processUnknownCallIntent(any(CallsManager.class),
+ intentCaptor.capture());
+ } else {
+ verify(mCallIntentProcessorAdapter).processIncomingCallIntent(any(CallsManager.class),
+ intentCaptor.capture());
+ }
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(expectedAction, capturedIntent.getAction());
+ Bundle intentExtras = capturedIntent.getExtras();
+ assertEquals(TEL_PA_HANDLE_CURRENT,
+ intentExtras.get(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
+ assertTrue(intentExtras.getBoolean(extraCallKey));
+
+ if (isUnknown) {
+ for (String expectedKey : expectedExtras.keySet()) {
+ assertTrue(intentExtras.containsKey(expectedKey));
+ assertEquals(expectedExtras.get(expectedKey), intentExtras.get(expectedKey));
+ }
+ }
+ else {
+ assertTrue(areBundlesEqual(expectedExtras,
+ (Bundle) intentExtras.get(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS)));
+ }
+ }
+
+ @SmallTest
+ public void testPlaceCallWithNonEmergencyPermission() throws Exception {
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString()))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingPermission(CALL_PHONE);
+
+ mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE);
+ placeCallTestHelper(handle, extras, true);
+ }
+
+ @SmallTest
+ public void testPlaceCallWithAppOpsOff() throws Exception {
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString()))
+ .thenReturn(AppOpsManager.MODE_IGNORED);
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .when(mContext).checkCallingPermission(CALL_PHONE);
+
+ mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE);
+ placeCallTestHelper(handle, extras, false);
+ }
+
+ @SmallTest
+ public void testPlaceCallWithNoCallingPermission() throws Exception {
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+
+ when(mAppOpsManager.noteOp(eq(AppOpsManager.OP_CALL_PHONE), anyInt(), anyString()))
+ .thenReturn(AppOpsManager.MODE_ALLOWED);
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mContext).checkCallingPermission(CALL_PHONE);
+
+ mTSIBinder.placeCall(handle, extras, DEFAULT_DIALER_PACKAGE);
+ placeCallTestHelper(handle, extras, false);
+ }
+
+ private void placeCallTestHelper(Uri expectedHandle, Bundle expectedExtras,
+ boolean shouldNonEmergencyBeAllowed) {
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mUserCallIntentProcessor).processIntent(intentCaptor.capture(), anyString(),
+ eq(shouldNonEmergencyBeAllowed));
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(Intent.ACTION_CALL, capturedIntent.getAction());
+ assertEquals(expectedHandle, capturedIntent.getData());
+ assertTrue(areBundlesEqual(expectedExtras, capturedIntent.getExtras()));
+ }
+
+ @SmallTest
+ public void testPlaceCallFailure() throws Exception {
+ Uri handle = Uri.parse("tel:6505551234");
+ Bundle extras = createSampleExtras();
+
+ doThrow(new SecurityException())
+ .when(mContext).enforceCallingOrSelfPermission(eq(CALL_PHONE), anyString());
+
+ try {
+ mTSIBinder.placeCall(handle, extras, "arbitrary_package_name");
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ verify(mUserCallIntentProcessor, never())
+ .processIntent(any(Intent.class), anyString(), anyBoolean());
+ }
+
+ @SmallTest
+ public void testSetDefaultDialer() throws Exception {
+ String packageName = "sample.package";
+
+ doReturn(true)
+ .when(mDefaultDialerManagerAdapter)
+ .setDefaultDialerApplication(any(Context.class), eq(packageName));
+
+ mTSIBinder.setDefaultDialer(packageName);
+
+ verify(mDefaultDialerManagerAdapter).setDefaultDialerApplication(any(Context.class),
+ eq(packageName));
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext).sendBroadcastAsUser(intentCaptor.capture(), any(UserHandle.class));
+ Intent capturedIntent = intentCaptor.getValue();
+ assertEquals(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED, capturedIntent.getAction());
+ String packageNameExtra = capturedIntent.getStringExtra(
+ TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME);
+ assertEquals(packageName, packageNameExtra);
+ }
+
+ @SmallTest
+ public void testSetDefaultDialerNoModifyPhoneStatePermission() throws Exception {
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ eq(MODIFY_PHONE_STATE), anyString());
+ setDefaultDialerFailureTestHelper();
+ }
+
+ @SmallTest
+ public void testSetDefaultDialerNoWriteSecureSettingsPermission() throws Exception {
+ doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission(
+ eq(WRITE_SECURE_SETTINGS), anyString());
+ setDefaultDialerFailureTestHelper();
+ }
+
+ private void setDefaultDialerFailureTestHelper() throws Exception {
+ boolean exceptionThrown = false;
+ try {
+ mTSIBinder.setDefaultDialer(DEFAULT_DIALER_PACKAGE);
+ } catch (SecurityException e) {
+ exceptionThrown = true;
+ }
+ assertTrue(exceptionThrown);
+ verify(mDefaultDialerManagerAdapter, never()).setDefaultDialerApplication(
+ any(Context.class), anyString());
+ verify(mContext, never()).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
+ }
+
+ @SmallTest
+ public void testIsVoicemailNumber() throws Exception {
+ String vmNumber = "010";
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_CURRENT);
+
+ doReturn(true).when(mFakePhoneAccountRegistrar).isVoiceMailNumber(TEL_PA_HANDLE_CURRENT,
+ vmNumber);
+ assertTrue(mTSIBinder.isVoiceMailNumber(TEL_PA_HANDLE_CURRENT,
+ vmNumber, DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testIsVoicemailNumberAccountNotVisibleFailure() throws Exception {
+ String vmNumber = "010";
+
+ doReturn(true).when(mFakePhoneAccountRegistrar).isVoiceMailNumber(TEL_PA_HANDLE_CURRENT,
+ vmNumber);
+
+ when(mFakePhoneAccountRegistrar.getPhoneAccount(TEL_PA_HANDLE_CURRENT,
+ Binder.getCallingUserHandle())).thenReturn(null);
+ assertFalse(mTSIBinder
+ .isVoiceMailNumber(TEL_PA_HANDLE_CURRENT, vmNumber, DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testGetVoicemailNumberWithNullAccountHandle() throws Exception {
+ when(mFakePhoneAccountRegistrar.getPhoneAccount(isNull(PhoneAccountHandle.class),
+ eq(Binder.getCallingUserHandle())))
+ .thenReturn(makePhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ int subId = 58374;
+ String vmNumber = "543";
+ doReturn(subId).when(mSubscriptionManagerAdapter).getDefaultVoiceSubId();
+
+ TelephonyManager mockTelephonyManager =
+ (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ when(mockTelephonyManager.getVoiceMailNumber(subId)).thenReturn(vmNumber);
+
+ assertEquals(vmNumber, mTSIBinder.getVoiceMailNumber(null, DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testGetVoicemailNumberWithNonNullAccountHandle() throws Exception {
+ when(mFakePhoneAccountRegistrar.getPhoneAccount(eq(TEL_PA_HANDLE_CURRENT),
+ eq(Binder.getCallingUserHandle())))
+ .thenReturn(makePhoneAccount(TEL_PA_HANDLE_CURRENT).build());
+ int subId = 58374;
+ String vmNumber = "543";
+
+ TelephonyManager mockTelephonyManager =
+ (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ when(mockTelephonyManager.getVoiceMailNumber(subId)).thenReturn(vmNumber);
+ when(mFakePhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(TEL_PA_HANDLE_CURRENT))
+ .thenReturn(subId);
+
+ assertEquals(vmNumber,
+ mTSIBinder.getVoiceMailNumber(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testGetLine1Number() throws Exception {
+ int subId = 58374;
+ String line1Number = "9482752023479";
+ makeAccountsVisibleToAllUsers(TEL_PA_HANDLE_CURRENT);
+ when(mFakePhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(TEL_PA_HANDLE_CURRENT))
+ .thenReturn(subId);
+ TelephonyManager mockTelephonyManager =
+ (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ when(mockTelephonyManager.getLine1Number(subId)).thenReturn(line1Number);
+
+ assertEquals(line1Number,
+ mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE));
+ }
+
+ @SmallTest
+ public void testEndCallWithRingingForegroundCall() throws Exception {
+ Call call = mock(Call.class);
+ when(call.getState()).thenReturn(CallState.RINGING);
+ when(mFakeCallsManager.getForegroundCall()).thenReturn(call);
+ assertTrue(mTSIBinder.endCall());
+ verify(call).reject(false, null);
+ }
+
+ @SmallTest
+ public void testEndCallWithNonRingingForegroundCall() throws Exception {
+ Call call = mock(Call.class);
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ when(mFakeCallsManager.getForegroundCall()).thenReturn(call);
+ assertTrue(mTSIBinder.endCall());
+ verify(call).disconnect();
+ }
+
+ @SmallTest
+ public void testEndCallWithNoForegroundCall() throws Exception {
+ Call call = mock(Call.class);
+ when(call.getState()).thenReturn(CallState.ACTIVE);
+ when(mFakeCallsManager.getFirstCallWithState(anyInt(), anyInt(), anyInt(), anyInt()))
+ .thenReturn(call);
+ assertTrue(mTSIBinder.endCall());
+ verify(call).disconnect();
+ }
+
+ @SmallTest
+ public void testEndCallWithNoCalls() throws Exception {
+ assertFalse(mTSIBinder.endCall());
+ }
+
+ @SmallTest
+ public void testAcceptRingingCall() throws Exception {
+ Call call = mock(Call.class);
+ when(mFakeCallsManager.getFirstCallWithState(any(int[].class)))
+ .thenReturn(call);
+ // Not intended to be a real video state. Here to ensure that the call will be answered
+ // with whatever video state it's currently in.
+ int fakeVideoState = 29578215;
+ when(call.getVideoState()).thenReturn(fakeVideoState);
+ mTSIBinder.acceptRingingCall();
+ verify(call).answer(fakeVideoState);
+ }
+
+ @SmallTest
+ public void testAcceptRingingCallWithValidVideoState() throws Exception {
+ Call call = mock(Call.class);
+ when(mFakeCallsManager.getFirstCallWithState(any(int[].class)))
+ .thenReturn(call);
+ // Not intended to be a real video state. Here to ensure that the call will be answered
+ // with the video state passed in to acceptRingingCallWithVideoState
+ int fakeVideoState = 29578215;
+ int realVideoState = VideoProfile.STATE_RX_ENABLED | VideoProfile.STATE_TX_ENABLED;
+ when(call.getVideoState()).thenReturn(fakeVideoState);
+ mTSIBinder.acceptRingingCallWithVideoState(realVideoState);
+ verify(call).answer(realVideoState);
+ }
+
+ /**
+ * Register phone accounts for the supplied PhoneAccountHandles to make them
+ * visible to all users (via the isVisibleToCaller method in TelecomServiceImpl.
+ * @param handles the handles for which phone accounts should be created for.
+ */
+ private void makeAccountsVisibleToAllUsers(PhoneAccountHandle... handles) {
+ for (PhoneAccountHandle ph : handles) {
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(eq(ph))).thenReturn(
+ makeMultiUserPhoneAccount(ph).build());
+ when(mFakePhoneAccountRegistrar
+ .getPhoneAccount(eq(ph), any(UserHandle.class), anyBoolean()))
+ .thenReturn(makeMultiUserPhoneAccount(ph).build());
+ when(mFakePhoneAccountRegistrar
+ .getPhoneAccount(eq(ph), any(UserHandle.class)))
+ .thenReturn(makeMultiUserPhoneAccount(ph).build());
+ }
+ }
+
+ private PhoneAccount.Builder makeMultiUserPhoneAccount(PhoneAccountHandle paHandle) {
+ PhoneAccount.Builder paBuilder = makePhoneAccount(paHandle);
+ paBuilder.setCapabilities(PhoneAccount.CAPABILITY_MULTI_USER);
+ return paBuilder;
+ }
+
+ private PhoneAccount.Builder makePhoneAccount(PhoneAccountHandle paHandle) {
+ return new PhoneAccount.Builder(paHandle, "testLabel");
+ }
+
+ private Bundle createSampleExtras() {
+ Bundle extras = new Bundle();
+ extras.putString("test_key", "test_value");
+ return extras;
+ }
+
+ private static boolean areBundlesEqual(Bundle b1, Bundle b2) {
+ for (String key1 : b1.keySet()) {
+ if (!b1.get(key1).equals(b2.get(key1))) {
+ return false;
+ }
+ }
+
+ for (String key2 : b2.keySet()) {
+ if (!b2.get(key2).equals(b1.get(key2))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 0e35f01..20f62f2 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -16,68 +16,126 @@
package com.android.server.telecom.tests;
-import com.google.common.base.Predicate;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
+import android.media.IAudioService;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Process;
import android.os.UserHandle;
import android.telecom.Call;
-import android.telecom.CallAudioState;
-import android.telecom.Connection;
import android.telecom.ConnectionRequest;
-import android.telecom.DisconnectCause;
import android.telecom.ParcelableCall;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
-import android.telephony.TelephonyManager;
import com.android.internal.telecom.IInCallAdapter;
+import com.android.server.telecom.BluetoothPhoneServiceImpl;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallerInfoAsyncQueryFactory;
import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.ContactsAsyncHelper;
import com.android.server.telecom.HeadsetMediaButton;
import com.android.server.telecom.HeadsetMediaButtonFactory;
import com.android.server.telecom.InCallWakeLockController;
import com.android.server.telecom.InCallWakeLockControllerFactory;
-import com.android.server.telecom.Log;
import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.ProximitySensorManager;
import com.android.server.telecom.ProximitySensorManagerFactory;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
+
+import com.google.common.base.Predicate;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
-import java.util.concurrent.BrokenBarrierException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.CyclicBarrier;
-
+/**
+ * Implements mocks and functionality required to implement telecom system tests.
+ */
public class TelecomSystemTest extends TelecomTestCase {
static final int TEST_POLL_INTERVAL = 10; // milliseconds
static final int TEST_TIMEOUT = 1000; // milliseconds
- @Mock MissedCallNotifier mMissedCallNotifier;
+ public class HeadsetMediaButtonFactoryF implements HeadsetMediaButtonFactory {
+ @Override
+ public HeadsetMediaButton create(Context context, CallsManager callsManager,
+ TelecomSystem.SyncRoot lock) {
+ return mHeadsetMediaButton;
+ }
+ }
+
+ public class ProximitySensorManagerFactoryF implements ProximitySensorManagerFactory {
+ @Override
+ public ProximitySensorManager create(Context context, CallsManager callsManager) {
+ return mProximitySensorManager;
+ }
+ }
+
+ public class InCallWakeLockControllerFactoryF implements InCallWakeLockControllerFactory {
+ @Override
+ public InCallWakeLockController create(Context context, CallsManager callsManager) {
+ return mInCallWakeLockController;
+ }
+ }
+
+ public static class MissedCallNotifierFakeImpl extends CallsManagerListenerBase
+ implements MissedCallNotifier {
+ @Override
+ public void clearMissedCalls(UserHandle userHandle) {
+
+ }
+
+ @Override
+ public void showMissedCallNotification(com.android.server.telecom.Call call) {
+
+ }
+
+ @Override
+ public void reloadFromDatabase(TelecomSystem.SyncRoot lock, CallsManager callsManager,
+ ContactsAsyncHelper contactsAsyncHelper,
+ CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory, UserHandle userHandle) {
+
+ }
+
+ @Override
+ public void setCurrentUserHandle(UserHandle userHandle) {
+
+ }
+ }
+
+ MissedCallNotifier mMissedCallNotifier = new MissedCallNotifierFakeImpl();
@Mock HeadsetMediaButton mHeadsetMediaButton;
@Mock ProximitySensorManager mProximitySensorManager;
@Mock InCallWakeLockController mInCallWakeLockController;
+ @Mock BluetoothPhoneServiceImpl mBluetoothPhoneServiceImpl;
final ComponentName mInCallServiceComponentNameX =
new ComponentName(
@@ -139,8 +197,14 @@
CallerInfoAsyncQueryFactoryFixture mCallerInfoAsyncQueryFactoryFixture;
+ IAudioService mAudioService;
+
TelecomSystem mTelecomSystem;
+ Context mSpyContext;
+
+ private int mNumOutgoingCallsMade;
+
class IdPair {
final String mConnectionId;
final String mCallId;
@@ -154,6 +218,10 @@
@Override
public void setUp() throws Exception {
super.setUp();
+ mSpyContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ doReturn(mSpyContext).when(mSpyContext).getApplicationContext();
+
+ mNumOutgoingCallsMade = 0;
// First set up information about the In-Call services in the mock Context, since
// Telecom will search for these as soon as it is instantiated
@@ -174,36 +242,48 @@
}
private void setupTelecomSystem() throws Exception {
+ // Use actual implementations instead of mocking the interface out.
HeadsetMediaButtonFactory headsetMediaButtonFactory =
- mock(HeadsetMediaButtonFactory.class);
+ spy(new HeadsetMediaButtonFactoryF());
ProximitySensorManagerFactory proximitySensorManagerFactory =
- mock(ProximitySensorManagerFactory.class);
+ spy(new ProximitySensorManagerFactoryF());
InCallWakeLockControllerFactory inCallWakeLockControllerFactory =
- mock(InCallWakeLockControllerFactory.class);
+ spy(new InCallWakeLockControllerFactoryF());
+ mAudioService = setupAudioService();
mCallerInfoAsyncQueryFactoryFixture = new CallerInfoAsyncQueryFactoryFixture();
- when(headsetMediaButtonFactory.create(
- any(Context.class),
- any(CallsManager.class),
- any(TelecomSystem.SyncRoot.class)))
- .thenReturn(mHeadsetMediaButton);
- when(proximitySensorManagerFactory.create(
- any(Context.class),
- any(CallsManager.class)))
- .thenReturn(mProximitySensorManager);
- when(inCallWakeLockControllerFactory.create(
- any(Context.class),
- any(CallsManager.class)))
- .thenReturn(mInCallWakeLockController);
-
mTelecomSystem = new TelecomSystem(
mComponentContextFixture.getTestDouble(),
- mMissedCallNotifier,
+ new MissedCallNotifierImplFactory() {
+ @Override
+ public MissedCallNotifier makeMissedCallNotifierImpl(Context context,
+ PhoneAccountRegistrar phoneAccountRegistrar) {
+ return mMissedCallNotifier;
+ }
+ },
mCallerInfoAsyncQueryFactoryFixture.getTestDouble(),
headsetMediaButtonFactory,
proximitySensorManagerFactory,
- inCallWakeLockControllerFactory);
+ inCallWakeLockControllerFactory,
+ new CallAudioManager.AudioServiceFactory() {
+ @Override
+ public IAudioService getAudioService() {
+ return mAudioService;
+ }
+ },
+ new BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory() {
+ @Override
+ public BluetoothPhoneServiceImpl makeBluetoothPhoneServiceImpl(Context context,
+ TelecomSystem.SyncRoot lock, CallsManager callsManager,
+ PhoneAccountRegistrar phoneAccountRegistrar) {
+ return mBluetoothPhoneServiceImpl;
+ }
+ });
+
+ mComponentContextFixture.setTelecomManager(new TelecomManager(
+ mComponentContextFixture.getTestDouble(),
+ mTelecomSystem.getTelecomServiceImpl().getBinder()));
verify(headsetMediaButtonFactory).create(
eq(mComponentContextFixture.getTestDouble().getApplicationContext()),
@@ -221,11 +301,9 @@
mConnectionServiceFixtureA = new ConnectionServiceFixture();
mConnectionServiceFixtureB = new ConnectionServiceFixture();
- mComponentContextFixture.addConnectionService(
- mConnectionServiceComponentNameA,
+ mComponentContextFixture.addConnectionService(mConnectionServiceComponentNameA,
mConnectionServiceFixtureA.getTestDouble());
- mComponentContextFixture.addConnectionService(
- mConnectionServiceComponentNameB,
+ mComponentContextFixture.addConnectionService(mConnectionServiceComponentNameB,
mConnectionServiceFixtureB.getTestDouble());
mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountA0);
@@ -233,7 +311,7 @@
mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountB0);
mTelecomSystem.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(
- mPhoneAccountA0.getAccountHandle());
+ mPhoneAccountA0.getAccountHandle(), Process.myUserHandle());
}
private void setupInCallServices() throws Exception {
@@ -243,34 +321,67 @@
mComponentContextFixture.putResource(
com.android.server.telecom.R.string.incall_default_class,
mInCallServiceComponentNameX.getClassName());
+ mComponentContextFixture.putBooleanResource(
+ com.android.internal.R.bool.config_voice_capable, true);
mInCallServiceFixtureX = new InCallServiceFixture();
mInCallServiceFixtureY = new InCallServiceFixture();
- mComponentContextFixture.addInCallService(
- mInCallServiceComponentNameX,
+ mComponentContextFixture.addInCallService(mInCallServiceComponentNameX,
mInCallServiceFixtureX.getTestDouble());
- mComponentContextFixture.addInCallService(
- mInCallServiceComponentNameY,
+ mComponentContextFixture.addInCallService(mInCallServiceComponentNameY,
mInCallServiceFixtureY.getTestDouble());
}
- private IdPair startOutgoingPhoneCall(
- String number,
- PhoneAccountHandle phoneAccountHandle,
- ConnectionServiceFixture connectionServiceFixture) throws Exception {
- reset(
- connectionServiceFixture.getTestDouble(),
- mInCallServiceFixtureX.getTestDouble(),
+ /**
+ * Helper method for setting up the fake audio service.
+ * Calls to the fake audio service need to toggle the return
+ * value of AudioManager#isMicrophoneMute.
+ * @return mock of IAudioService
+ */
+ private IAudioService setupAudioService() {
+ IAudioService audioService = mock(IAudioService.class);
+
+ final AudioManager fakeAudioManager =
+ (AudioManager) mComponentContextFixture.getTestDouble()
+ .getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+
+ try {
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock i) {
+ Object[] args = i.getArguments();
+ doReturn(args[0]).when(fakeAudioManager).isMicrophoneMute();
+ return null;
+ }
+ }).when(audioService)
+ .setMicrophoneMute(any(Boolean.class), any(String.class), any(Integer.class));
+
+ } catch (android.os.RemoteException e) {
+ // Do nothing, leave the faked microphone state as-is
+ }
+ return audioService;
+ }
+
+ protected IdPair startOutgoingPhoneCall(String number, PhoneAccountHandle phoneAccountHandle,
+ ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser)
+ throws Exception {
+ return startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture,
+ initiatingUser, VideoProfile.STATE_AUDIO_ONLY);
+ }
+
+ protected IdPair startOutgoingPhoneCall(String number, PhoneAccountHandle phoneAccountHandle,
+ ConnectionServiceFixture connectionServiceFixture, UserHandle initiatingUser,
+ int videoState) throws Exception {
+ reset(connectionServiceFixture.getTestDouble(), mInCallServiceFixtureX.getTestDouble(),
mInCallServiceFixtureY.getTestDouble());
- assertEquals(
- mInCallServiceFixtureX.mCallById.size(),
+ assertEquals(mInCallServiceFixtureX.mCallById.size(),
mInCallServiceFixtureY.mCallById.size());
- assertEquals(
- (mInCallServiceFixtureX.mInCallAdapter != null),
+ assertEquals((mInCallServiceFixtureX.mInCallAdapter != null),
(mInCallServiceFixtureY.mInCallAdapter != null));
+ mNumOutgoingCallsMade++;
int startingNumConnections = connectionServiceFixture.mConnectionById.size();
int startingNumCalls = mInCallServiceFixtureX.mCallById.size();
boolean hasInCallAdapter = mInCallServiceFixtureX.mInCallAdapter != null;
@@ -284,7 +395,18 @@
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
phoneAccountHandle);
}
+ if (videoState != VideoProfile.STATE_AUDIO_ONLY) {
+ actionCallIntent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
+ }
+ final UserHandle userHandle = initiatingUser;
+ Context localAppContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ new UserCallIntentProcessor(localAppContext, userHandle).processIntent(
+ actionCallIntent, null, true /* hasCallAppOp*/);
+ // UserCallIntentProcessor's mContext.sendBroadcastAsUser(...) will call to an empty method
+ // as to not actually try to send an intent to PrimaryCallReceiver. We verify that it was
+ // called correctly in order to continue.
+ verify(localAppContext).sendBroadcastAsUser(actionCallIntent, UserHandle.SYSTEM);
mTelecomSystem.getCallIntentProcessor().processIntent(actionCallIntent);
if (!hasInCallAdapter) {
@@ -301,7 +423,8 @@
ArgumentCaptor<BroadcastReceiver> newOutgoingCallReceiver =
ArgumentCaptor.forClass(BroadcastReceiver.class);
- verify(mComponentContextFixture.getTestDouble().getApplicationContext())
+ verify(mComponentContextFixture.getTestDouble().getApplicationContext(),
+ times(mNumOutgoingCallsMade))
.sendOrderedBroadcastAsUser(
newOutgoingCallIntent.capture(),
any(UserHandle.class),
@@ -319,35 +442,27 @@
new BroadcastReceiver.PendingResult(0, "", null, 0, true, false, null, 0, 0));
newOutgoingCallReceiver.getValue().setResultData(
newOutgoingCallIntent.getValue().getStringExtra(Intent.EXTRA_PHONE_NUMBER));
- newOutgoingCallReceiver.getValue().onReceive(
- mComponentContextFixture.getTestDouble(),
+ newOutgoingCallReceiver.getValue().onReceive(mComponentContextFixture.getTestDouble(),
newOutgoingCallIntent.getValue());
assertEquals(startingNumConnections + 1, connectionServiceFixture.mConnectionById.size());
- verify(connectionServiceFixture.getTestDouble()).createConnection(
- eq(phoneAccountHandle),
- anyString(),
- any(ConnectionRequest.class),
- anyBoolean(),
- anyBoolean());
-
+ verify(connectionServiceFixture.getTestDouble())
+ .createConnection(eq(phoneAccountHandle), anyString(), any(ConnectionRequest.class),
+ anyBoolean(), anyBoolean());
connectionServiceFixture.sendHandleCreateConnectionComplete(
connectionServiceFixture.mLatestConnectionId);
assertEquals(startingNumCalls + 1, mInCallServiceFixtureX.mCallById.size());
assertEquals(startingNumCalls + 1, mInCallServiceFixtureY.mCallById.size());
- assertEquals(
- mInCallServiceFixtureX.mLatestCallId,
- mInCallServiceFixtureY.mLatestCallId);
+ assertEquals(mInCallServiceFixtureX.mLatestCallId, mInCallServiceFixtureY.mLatestCallId);
- return new IdPair(
- connectionServiceFixture.mLatestConnectionId,
+ return new IdPair(connectionServiceFixture.mLatestConnectionId,
mInCallServiceFixtureX.mLatestCallId);
}
- private IdPair startIncomingPhoneCall(
+ protected IdPair startIncomingPhoneCall(
String number,
PhoneAccountHandle phoneAccountHandle,
final ConnectionServiceFixture connectionServiceFixture) throws Exception {
@@ -355,26 +470,22 @@
connectionServiceFixture);
}
- private IdPair startIncomingPhoneCall(
+ protected IdPair startIncomingPhoneCall(
String number,
PhoneAccountHandle phoneAccountHandle,
int videoState,
final ConnectionServiceFixture connectionServiceFixture) throws Exception {
- reset(
- connectionServiceFixture.getTestDouble(),
- mInCallServiceFixtureX.getTestDouble(),
+ reset(connectionServiceFixture.getTestDouble(), mInCallServiceFixtureX.getTestDouble(),
mInCallServiceFixtureY.getTestDouble());
- assertEquals(
- mInCallServiceFixtureX.mCallById.size(),
+ assertEquals(mInCallServiceFixtureX.mCallById.size(),
mInCallServiceFixtureY.mCallById.size());
- assertEquals(
- (mInCallServiceFixtureX.mInCallAdapter != null),
+ assertEquals((mInCallServiceFixtureX.mInCallAdapter != null),
(mInCallServiceFixtureY.mInCallAdapter != null));
-
final int startingNumConnections = connectionServiceFixture.mConnectionById.size();
final int startingNumCalls = mInCallServiceFixtureX.mCallById.size();
boolean hasInCallAdapter = mInCallServiceFixtureX.mInCallAdapter != null;
+ connectionServiceFixture.mConnectionServiceDelegate.mVideoState = videoState;
Bundle extras = new Bundle();
extras.putParcelable(
@@ -383,22 +494,16 @@
mTelecomSystem.getTelecomServiceImpl().getBinder()
.addNewIncomingCall(phoneAccountHandle, extras);
- verify(connectionServiceFixture.getTestDouble()).createConnection(
- any(PhoneAccountHandle.class),
- anyString(),
- any(ConnectionRequest.class),
- eq(true),
- eq(false));
+ verify(connectionServiceFixture.getTestDouble())
+ .createConnection(any(PhoneAccountHandle.class), anyString(),
+ any(ConnectionRequest.class), eq(true), eq(false));
- mConnectionServiceFixtureA.mConnectionById.get(
- connectionServiceFixture.mLatestConnectionId).videoState = videoState;
+ connectionServiceFixture.sendSetRinging(connectionServiceFixture.mLatestConnectionId);
- connectionServiceFixture.sendHandleCreateConnectionComplete(
- connectionServiceFixture.mLatestConnectionId);
- connectionServiceFixture.sendSetRinging(
- connectionServiceFixture.mLatestConnectionId);
- connectionServiceFixture.sendSetVideoState(
- connectionServiceFixture.mLatestConnectionId);
+ for (CallerInfoAsyncQueryFactoryFixture.Request request :
+ mCallerInfoAsyncQueryFactoryFixture.mRequests) {
+ request.reply();
+ }
// For the case of incoming calls, Telecom connecting the InCall services and adding the
// Call is triggered by the async completion of the CallerInfoAsyncQuery. Once the Call
@@ -406,16 +511,10 @@
// test fixtures, will be synchronous.
if (!hasInCallAdapter) {
- verify(
- mInCallServiceFixtureX.getTestDouble(),
- timeout(TEST_TIMEOUT))
- .setInCallAdapter(
- any(IInCallAdapter.class));
- verify(
- mInCallServiceFixtureY.getTestDouble(),
- timeout(TEST_TIMEOUT))
- .setInCallAdapter(
- any(IInCallAdapter.class));
+ verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
+ .setInCallAdapter(any(IInCallAdapter.class));
+ verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT))
+ .setInCallAdapter(any(IInCallAdapter.class));
}
// Give the InCallService time to respond
@@ -434,16 +533,10 @@
}
});
- verify(
- mInCallServiceFixtureX.getTestDouble(),
- timeout(TEST_TIMEOUT))
- .addCall(
- any(ParcelableCall.class));
- verify(
- mInCallServiceFixtureY.getTestDouble(),
- timeout(TEST_TIMEOUT))
- .addCall(
- any(ParcelableCall.class));
+ verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
+ .addCall(any(ParcelableCall.class));
+ verify(mInCallServiceFixtureY.getTestDouble(), timeout(TEST_TIMEOUT))
+ .addCall(any(ParcelableCall.class));
// Give the InCallService time to respond
@@ -467,53 +560,35 @@
}
});
- assertEquals(
- mInCallServiceFixtureX.mLatestCallId,
- mInCallServiceFixtureY.mLatestCallId);
+ assertEquals(mInCallServiceFixtureX.mLatestCallId, mInCallServiceFixtureY.mLatestCallId);
- return new IdPair(
- connectionServiceFixture.mLatestConnectionId,
+ return new IdPair(connectionServiceFixture.mLatestConnectionId,
mInCallServiceFixtureX.mLatestCallId);
}
- private void rapidFire(Runnable... tasks) {
- final CyclicBarrier barrier = new CyclicBarrier(tasks.length);
- final CountDownLatch latch = new CountDownLatch(tasks.length);
- for (int i = 0; i < tasks.length; i++) {
- final Runnable task = tasks[i];
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- task.run();
- } catch (InterruptedException | BrokenBarrierException e){
- Log.e(TelecomSystemTest.this, e, "Unexpectedly interrupted");
- } finally {
- latch.countDown();
- }
- }
- }).start();
- }
- try {
- latch.await();
- } catch (InterruptedException e) {
- Log.e(TelecomSystemTest.this, e, "Unexpectedly interrupted");
- }
+ protected IdPair startAndMakeActiveOutgoingCall(
+ String number,
+ PhoneAccountHandle phoneAccountHandle,
+ ConnectionServiceFixture connectionServiceFixture) throws Exception {
+ return startAndMakeActiveOutgoingCall(number, phoneAccountHandle, connectionServiceFixture,
+ VideoProfile.STATE_AUDIO_ONLY);
}
// A simple outgoing call, verifying that the appropriate connection service is contacted,
// the proper lifecycle is followed, and both In-Call Services are updated correctly.
- private IdPair startAndMakeActiveOutgoingCall(
+ protected IdPair startAndMakeActiveOutgoingCall(
String number,
PhoneAccountHandle phoneAccountHandle,
- ConnectionServiceFixture connectionServiceFixture) throws Exception {
- IdPair ids = startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture);
+ ConnectionServiceFixture connectionServiceFixture, int videoState) throws Exception {
+ IdPair ids = startOutgoingPhoneCall(number, phoneAccountHandle, connectionServiceFixture,
+ Process.myUserHandle(), videoState);
connectionServiceFixture.sendSetDialing(ids.mConnectionId);
assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
assertEquals(Call.STATE_DIALING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
+ connectionServiceFixture.sendSetVideoState(ids.mConnectionId);
+
connectionServiceFixture.sendSetActive(ids.mConnectionId);
assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
@@ -521,144 +596,35 @@
return ids;
}
- public void testSingleOutgoingCallLocalDisconnect() throws Exception {
- IdPair ids = startAndMakeActiveOutgoingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
-
- mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
- assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
-
- mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureY.getCall(ids.mCallId).getState());
- }
-
- public void testSingleOutgoingCallRemoteDisconnect() throws Exception {
- IdPair ids = startAndMakeActiveOutgoingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
-
- mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureY.getCall(ids.mCallId).getState());
- }
-
- /**
- * Tests the {@link TelecomManager#acceptRingingCall()} API. Tests simple case of an incoming
- * audio-only call.
- *
- * @throws Exception
- */
- public void testTelecomManagerAcceptRingingCall() throws Exception {
- IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
-
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
-
- // Use TelecomManager API to answer the ringing call.
- TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
- .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
- telecomManager.acceptRingingCall();
-
- verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
- .answer(ids.mCallId);
- }
-
- /**
- * Tests the {@link TelecomManager#acceptRingingCall()} API. Tests simple case of an incoming
- * video call, which should be answered as video.
- *
- * @throws Exception
- */
- public void testTelecomManagerAcceptRingingVideoCall() throws Exception {
- IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
-
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
-
- // Use TelecomManager API to answer the ringing call; the default expected behavior is to
- // answer using whatever video state the ringing call requests.
- TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
- .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
- telecomManager.acceptRingingCall();
-
- // Answer video API should be called
- verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
- .answerVideo(eq(ids.mCallId), eq(VideoProfile.STATE_BIDIRECTIONAL));
- }
-
- /**
- * Tests the {@link TelecomManager#acceptRingingCall(int)} API. Tests answering a video call
- * as an audio call.
- *
- * @throws Exception
- */
- public void testTelecomManagerAcceptRingingVideoCallAsAudio() throws Exception {
- IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
-
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
-
- // Use TelecomManager API to answer the ringing call.
- TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
- .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
- telecomManager.acceptRingingCall(VideoProfile.STATE_AUDIO_ONLY);
-
- // The generic answer method on the ConnectionService is used to answer audio-only calls.
- verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
- .answer(eq(ids.mCallId));
- }
-
- /**
- * Tests the {@link TelecomManager#acceptRingingCall()} API. Tests simple case of an incoming
- * video call, where an attempt is made to answer with an invalid video state.
- *
- * @throws Exception
- */
- public void testTelecomManagerAcceptRingingInvalidVideoState() throws Exception {
- IdPair ids = startIncomingPhoneCall("650-555-1212", mPhoneAccountA0.getAccountHandle(),
- VideoProfile.STATE_BIDIRECTIONAL, mConnectionServiceFixtureA);
-
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
-
- // Use TelecomManager API to answer the ringing call; the default expected behavior is to
- // answer using whatever video state the ringing call requests.
- TelecomManager telecomManager = (TelecomManager) mComponentContextFixture.getTestDouble()
- .getApplicationContext().getSystemService(Context.TELECOM_SERVICE);
- telecomManager.acceptRingingCall(999 /* invalid videostate */);
-
- // Answer video API should be called
- verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
- .answerVideo(eq(ids.mCallId), eq(VideoProfile.STATE_BIDIRECTIONAL));
- }
-
- // A simple incoming call, similar in scope to the previous test
- private IdPair startAndMakeActiveIncomingCall(
+ protected IdPair startAndMakeActiveIncomingCall(
String number,
PhoneAccountHandle phoneAccountHandle,
ConnectionServiceFixture connectionServiceFixture) throws Exception {
+ return startAndMakeActiveIncomingCall(number, phoneAccountHandle, connectionServiceFixture,
+ VideoProfile.STATE_AUDIO_ONLY);
+ }
+
+ // A simple incoming call, similar in scope to the previous test
+ protected IdPair startAndMakeActiveIncomingCall(
+ String number,
+ PhoneAccountHandle phoneAccountHandle,
+ ConnectionServiceFixture connectionServiceFixture,
+ int videoState) throws Exception {
IdPair ids = startIncomingPhoneCall(number, phoneAccountHandle, connectionServiceFixture);
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
assertEquals(Call.STATE_RINGING, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
mInCallServiceFixtureX.mInCallAdapter
- .answerCall(ids.mCallId, VideoProfile.STATE_AUDIO_ONLY);
+ .answerCall(ids.mCallId, videoState);
- verify(connectionServiceFixture.getTestDouble())
- .answer(ids.mConnectionId);
+ if (!VideoProfile.isVideo(videoState)) {
+ verify(connectionServiceFixture.getTestDouble())
+ .answer(ids.mConnectionId);
+ } else {
+ verify(connectionServiceFixture.getTestDouble())
+ .answerVideo(ids.mConnectionId, videoState);
+ }
connectionServiceFixture.sendSetActive(ids.mConnectionId);
assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
@@ -667,146 +633,6 @@
return ids;
}
- public void testSingleIncomingCallLocalDisconnect() throws Exception {
- IdPair ids = startAndMakeActiveIncomingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
-
- mInCallServiceFixtureX.mInCallAdapter.disconnectCall(ids.mCallId);
- assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureY.getCall(ids.mCallId).getState());
-
- mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureY.getCall(ids.mCallId).getState());
- }
-
- public void testSingleIncomingCallRemoteDisconnect() throws Exception {
- IdPair ids = startAndMakeActiveIncomingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
-
- mConnectionServiceFixtureA.sendSetDisconnected(ids.mConnectionId, DisconnectCause.LOCAL);
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureX.getCall(ids.mCallId).getState());
- assertEquals(Call.STATE_DISCONNECTED,
- mInCallServiceFixtureY.getCall(ids.mCallId).getState());
- }
-
- public void do_testDeadlockOnOutgoingCall() throws Exception {
- final IdPair ids = startOutgoingPhoneCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
- rapidFire(
- new Runnable() {
- @Override
- public void run() {
- while (mCallerInfoAsyncQueryFactoryFixture.mRequests.size() > 0) {
- mCallerInfoAsyncQueryFactoryFixture.mRequests.remove(0).reply();
- }
- }
- },
- new Runnable() {
- @Override
- public void run() {
- try {
- mConnectionServiceFixtureA.sendSetActive(ids.mConnectionId);
- } catch (Exception e) {
- Log.e(this, e, "");
- }
- }
- });
- }
-
- public void testDeadlockOnOutgoingCall() throws Exception {
- for (int i = 0; i < 100; i++) {
- TelecomSystemTest test = new TelecomSystemTest();
- test.setContext(getContext());
- test.setTestContext(getTestContext());
- test.setName(getName());
- test.setUp();
- test.do_testDeadlockOnOutgoingCall();
- test.tearDown();
- }
- }
-
- public void testIncomingThenOutgoingCalls() throws Exception {
- // TODO: We have to use the same PhoneAccount for both; see http://b/18461539
- IdPair incoming = startAndMakeActiveIncomingCall(
- "650-555-2323",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
- IdPair outgoing = startAndMakeActiveOutgoingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
- }
-
- public void testOutgoingThenIncomingCalls() throws Exception {
- // TODO: We have to use the same PhoneAccount for both; see http://b/18461539
- IdPair outgoing = startAndMakeActiveOutgoingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
- IdPair incoming = startAndMakeActiveIncomingCall(
- "650-555-2323",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
- verify(mConnectionServiceFixtureA.getTestDouble())
- .hold(outgoing.mConnectionId);
- mConnectionServiceFixtureA.mConnectionById.get(outgoing.mConnectionId).state =
- Connection.STATE_HOLDING;
- mConnectionServiceFixtureA.sendSetOnHold(outgoing.mConnectionId);
- assertEquals(
- Call.STATE_HOLDING,
- mInCallServiceFixtureX.getCall(outgoing.mCallId).getState());
- assertEquals(
- Call.STATE_HOLDING,
- mInCallServiceFixtureY.getCall(outgoing.mCallId).getState());
- }
-
- public void testAudioManagerOperations() throws Exception {
- AudioManager audioManager = (AudioManager) mComponentContextFixture.getTestDouble()
- .getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
-
- IdPair outgoing = startAndMakeActiveOutgoingCall(
- "650-555-1212",
- mPhoneAccountA0.getAccountHandle(),
- mConnectionServiceFixtureA);
-
- verify(audioManager, timeout(TEST_TIMEOUT))
- .requestAudioFocusForCall(anyInt(), anyInt());
- verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce())
- .setMode(AudioManager.MODE_IN_CALL);
-
- mInCallServiceFixtureX.mInCallAdapter.mute(true);
- verify(audioManager, timeout(TEST_TIMEOUT))
- .setMicrophoneMute(true);
- mInCallServiceFixtureX.mInCallAdapter.mute(false);
- verify(audioManager, timeout(TEST_TIMEOUT))
- .setMicrophoneMute(false);
-
- mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
- verify(audioManager, timeout(TEST_TIMEOUT))
- .setSpeakerphoneOn(true);
- mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE);
- verify(audioManager, timeout(TEST_TIMEOUT))
- .setSpeakerphoneOn(false);
-
- mConnectionServiceFixtureA.
- sendSetDisconnected(outgoing.mConnectionId, DisconnectCause.REMOTE);
-
- verify(audioManager, timeout(TEST_TIMEOUT))
- .abandonAudioFocusForCall();
- verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce())
- .setMode(AudioManager.MODE_NORMAL);
- }
-
protected static void assertTrueWithTimeout(Predicate<Void> predicate) {
int elapsed = 0;
while (elapsed < TEST_TIMEOUT) {
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index 144ef66..ee4b72e 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -23,7 +23,7 @@
import android.test.AndroidTestCase;
public abstract class TelecomTestCase extends AndroidTestCase {
- private static final String TESTING_TAG = "Telecom-TEST";
+ protected static final String TESTING_TAG = "Telecom-TEST";
MockitoHelper mMockitoHelper = new MockitoHelper();
ComponentContextFixture mComponentContextFixture;
@@ -33,6 +33,7 @@
Log.setTag(TESTING_TAG);
mMockitoHelper.setUp(getContext(), getClass());
mComponentContextFixture = new ComponentContextFixture();
+ Log.setContext(mComponentContextFixture.getTestDouble().getApplicationContext());
MockitoAnnotations.initMocks(this);
}
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
new file mode 100644
index 0000000..2a54034
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 org.mockito.ArgumentCaptor;
+
+import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
+
+import java.util.List;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+/**
+ * System tests for video-specific behavior in telecom.
+ * TODO: Add unit tests which ensure that auto-speakerphone does not occur when using a wired
+ * headset or a bluetooth headset.
+ */
+public class VideoCallTests extends TelecomSystemTest {
+
+ /**
+ * Tests to ensure an incoming video-call is automatically routed to the speakerphone when
+ * the call is answered and neither a wired headset nor bluetooth headset are connected.
+ */
+ public void testAutoSpeakerphoneIncomingBidirectional() throws Exception {
+ // Start an incoming video call.
+ IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ VideoProfile.STATE_BIDIRECTIONAL);
+
+ verifyAudioRoute(CallAudioState.ROUTE_SPEAKER, 2);
+ }
+
+ /**
+ * Tests to ensure an incoming receive-only video-call is answered in speakerphone mode. Note
+ * that this is not a scenario we would expect normally with the default dialer as it will
+ * always answer incoming video calls as bi-directional. It is, however, possible for a third
+ * party dialer to answer an incoming video call a a one-way video call.
+ */
+ public void testAutoSpeakerphoneIncomingReceiveOnly() throws Exception {
+ // Start an incoming video call.
+ IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ VideoProfile.STATE_RX_ENABLED);
+
+ verifyAudioRoute(CallAudioState.ROUTE_SPEAKER, 2);
+ }
+
+ /**
+ * Tests audio routing for an outgoing video call made with bidirectional video. Expect to be
+ * in speaker mode.
+ */
+ public void testAutoSpeakerphoneOutgoingBidirectional() throws Exception {
+ // Start an incoming video call.
+ IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ VideoProfile.STATE_BIDIRECTIONAL);
+
+ verifyAudioRoute(CallAudioState.ROUTE_SPEAKER, 2);
+ }
+
+ /**
+ * Tests audio routing for an outgoing video call made with transmit only video. Expect to be
+ * in speaker mode. Note: The default UI does not support making one-way video calls, but the
+ * APIs do and a third party incall UI could choose to support that.
+ */
+ public void testAutoSpeakerphoneOutgoingTransmitOnly() throws Exception {
+ // Start an incoming video call.
+ IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ VideoProfile.STATE_TX_ENABLED);
+
+ verifyAudioRoute(CallAudioState.ROUTE_SPEAKER, 2);
+ }
+
+ /**
+ * Tests audio routing for an outgoing video call made with transmit only video. Expect to be
+ * in speaker mode. Note: The default UI does not support making one-way video calls, but the
+ * APIs do and a third party incall UI could choose to support that.
+ */
+ public void testNoAutoSpeakerphoneOnOutgoing() throws Exception {
+ // Start an incoming video call.
+ IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ VideoProfile.STATE_AUDIO_ONLY);
+
+ verifyAudioRoute(CallAudioState.ROUTE_EARPIECE, 1);
+ }
+
+ /**
+ * Tests to ensure an incoming audio-only call is routed to the earpiece.
+ */
+ public void testNoAutoSpeakerphoneOnIncoming() throws Exception {
+
+ // Start an incoming video call.
+ IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+ mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA,
+ VideoProfile.STATE_AUDIO_ONLY);
+
+ verifyAudioRoute(CallAudioState.ROUTE_EARPIECE, 1);
+ }
+
+ /**
+ * Verifies that the
+ * {@link android.telecom.InCallService#onCallAudioStateChanged(CallAudioState)} change is
+ * called with an expected route and number of changes.
+ *
+ * @param expectedRoute The expected audio route on the latest change.
+ * @param audioStateChangeCount The number of audio state changes expected. This is set based
+ * on how many times we expect the audio route to change when
+ * setting up a call. For an audio-only call, we normally expect
+ * 1 route change, and for a video call we expect an extra change.
+ */
+ private void verifyAudioRoute(int expectedRoute, int audioStateChangeCount) throws Exception {
+ // Capture all onCallAudioStateChanged callbacks to InCall.
+ ArgumentCaptor<CallAudioState> callAudioStateArgumentCaptor = ArgumentCaptor.forClass(
+ CallAudioState.class);
+ verify(mInCallServiceFixtureX.getTestDouble(),
+ timeout(TEST_TIMEOUT).times(audioStateChangeCount)).
+ onCallAudioStateChanged(callAudioStateArgumentCaptor.capture());
+ List<CallAudioState> changes = callAudioStateArgumentCaptor.getAllValues();
+ assertEquals(expectedRoute, changes.get(changes.size() - 1).getRoute());
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
new file mode 100644
index 0000000..bc7f9e1
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.server.telecom.Log;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.internal.exceptions.ExceptionIncludingMockitoWarnings;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Camera;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.ParcelableCall;
+import android.telecom.TelecomManager;
+import android.telecom.VideoCallImpl;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.view.Surface;
+
+import com.google.common.base.Predicate;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.RunnableFuture;
+import java.util.concurrent.TimeUnit;
+
+import static android.test.MoreAsserts.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Performs tests of the {@link VideoProvider} and {@link VideoCall} APIs. Ensures that requests
+ * sent from an InCallService are routed through Telecom to a VideoProvider, and that callbacks are
+ * correctly routed.
+ */
+public class VideoProviderTest extends TelecomSystemTest {
+ private static final int ORIENTATION_0 = 0;
+ private static final int ORIENTATION_90 = 90;
+ private static final float ZOOM_LEVEL = 3.0f;
+
+ @Mock private VideoCall.Callback mVideoCallCallback;
+ private IdPair mCallIds;
+ private InCallService.VideoCall mVideoCall;
+ private VideoCallImpl mVideoCallImpl;
+ private ConnectionServiceFixture.ConnectionInfo mConnectionInfo;
+ private CountDownLatch mVerificationLock;
+
+ private Answer mVerification = new Answer() {
+ @Override
+ public Object answer(InvocationOnMock i) {
+ mVerificationLock.countDown();
+ return null;
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mCallIds = startAndMakeActiveOutgoingCall(
+ "650-555-1212",
+ mPhoneAccountA0.getAccountHandle(),
+ mConnectionServiceFixtureA);
+
+ // Set the video provider on the connection.
+ mConnectionServiceFixtureA.sendSetVideoProvider(
+ mConnectionServiceFixtureA.mLatestConnectionId);
+
+ // Provide a mocked VideoCall.Callback to receive callbacks via.
+ mVideoCallCallback = mock(InCallService.VideoCall.Callback.class);
+
+ mVideoCall = mInCallServiceFixtureX.getCall(mCallIds.mCallId).getVideoCallImpl();
+ mVideoCallImpl = (VideoCallImpl) mVideoCall;
+ mVideoCall.registerCallback(mVideoCallCallback);
+
+ mConnectionInfo = mConnectionServiceFixtureA.mConnectionById.get(mCallIds.mConnectionId);
+ mVerificationLock = new CountDownLatch(1);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Tests the {@link VideoCall#setCamera(String)}, {@link VideoProvider#onSetCamera(String)},
+ * and {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)}
+ * APIS.
+ */
+ public void testCameraChange() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onCameraCapabilitiesChanged(any(CameraCapabilities.class));
+
+ // Make 2 setCamera requests.
+ mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
+ mVideoCall.setCamera(MockVideoProvider.CAMERA_BACK);
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Capture the video profile reported via the callback.
+ ArgumentCaptor<CameraCapabilities> cameraCapabilitiesCaptor =
+ ArgumentCaptor.forClass(CameraCapabilities.class);
+
+ // Verify that the callback was called twice and capture the callback arguments.
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT).times(2))
+ .onCameraCapabilitiesChanged(cameraCapabilitiesCaptor.capture());
+
+ assertEquals(2, cameraCapabilitiesCaptor.getAllValues().size());
+
+ List<CameraCapabilities> cameraCapabilities = cameraCapabilitiesCaptor.getAllValues();
+ // Ensure dimensions are as expected.
+ assertEquals(MockVideoProvider.CAMERA_FRONT_DIMENSIONS,
+ cameraCapabilities.get(0).getHeight());
+ assertEquals(MockVideoProvider.CAMERA_BACK_DIMENSIONS,
+ cameraCapabilities.get(1).getHeight());
+ }
+
+ /**
+ * Tests the {@link VideoCall#setPreviewSurface(Surface)} and
+ * {@link VideoProvider#onSetPreviewSurface(Surface)} APIs.
+ */
+ public void testSetPreviewSurface() throws Exception {
+ final Surface surface = new Surface(new SurfaceTexture(1));
+ mVideoCall.setPreviewSurface(surface);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getPreviewSurface() == surface;
+ }
+ });
+
+ mVideoCall.setPreviewSurface(null);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getPreviewSurface() == null;
+ }
+ });
+ }
+
+ /**
+ * Tests the {@link VideoCall#setDisplaySurface(Surface)} and
+ * {@link VideoProvider#onSetDisplaySurface(Surface)} APIs.
+ */
+ public void testSetDisplaySurface() throws Exception {
+ final Surface surface = new Surface(new SurfaceTexture(1));
+ mVideoCall.setDisplaySurface(surface);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getDisplaySurface() == surface;
+ }
+ });
+
+ mVideoCall.setDisplaySurface(null);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getDisplaySurface() == null;
+ }
+ });
+ }
+
+ /**
+ * Tests the {@link VideoCall#setDeviceOrientation(int)} and
+ * {@link VideoProvider#onSetDeviceOrientation(int)} APIs.
+ */
+ public void testSetDeviceOrientation() throws Exception {
+ mVideoCall.setDeviceOrientation(ORIENTATION_0);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getDeviceOrientation() == ORIENTATION_0;
+ }
+ });
+
+ mVideoCall.setDeviceOrientation(ORIENTATION_90);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getDeviceOrientation() == ORIENTATION_90;
+ }
+ });
+ }
+
+ /**
+ * Tests the {@link VideoCall#setZoom(float)} and {@link VideoProvider#onSetZoom(float)} APIs.
+ */
+ public void testSetZoom() throws Exception {
+ mVideoCall.setZoom(ZOOM_LEVEL);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ return mConnectionInfo.mockVideoProvider.getZoom() == ZOOM_LEVEL;
+ }
+ });
+ }
+
+ /**
+ * Tests the {@link VideoCall#sendSessionModifyRequest(VideoProfile)},
+ * {@link VideoProvider#onSendSessionModifyRequest(VideoProfile, VideoProfile)},
+ * {@link VideoProvider#receiveSessionModifyResponse(int, VideoProfile, VideoProfile)}, and
+ * {@link VideoCall.Callback#onSessionModifyResponseReceived(int, VideoProfile, VideoProfile)}
+ * APIs.
+ *
+ * Emulates a scenario where an InCallService sends a request to upgrade to video, which the
+ * peer accepts as-is.
+ */
+ public void testSessionModifyRequest() throws Exception {
+ VideoProfile requestProfile = new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL);
+
+ // Set the starting video state on the video call impl; normally this would be set based on
+ // the original android.telecom.Call instance.
+ mVideoCallImpl.setVideoState(VideoProfile.STATE_RX_ENABLED);
+
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onSessionModifyResponseReceived(anyInt(), any(VideoProfile.class),
+ any(VideoProfile.class));
+
+ // Send the request.
+ mVideoCall.sendSessionModifyRequest(requestProfile);
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Capture the video profiles from the callback.
+ ArgumentCaptor<VideoProfile> fromVideoProfileCaptor =
+ ArgumentCaptor.forClass(VideoProfile.class);
+ ArgumentCaptor<VideoProfile> toVideoProfileCaptor =
+ ArgumentCaptor.forClass(VideoProfile.class);
+
+ // Verify we got a response and capture the profiles.
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onSessionModifyResponseReceived(eq(VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS),
+ fromVideoProfileCaptor.capture(), toVideoProfileCaptor.capture());
+
+ assertEquals(VideoProfile.STATE_RX_ENABLED,
+ fromVideoProfileCaptor.getValue().getVideoState());
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+ toVideoProfileCaptor.getValue().getVideoState());
+ }
+
+ /**
+ * Tests the {@link VideoCall#sendSessionModifyResponse(VideoProfile)},
+ * and {@link VideoProvider#onSendSessionModifyResponse(VideoProfile)} APIs.
+ */
+ public void testSessionModifyResponse() throws Exception {
+ VideoProfile sessionModifyResponse = new VideoProfile(VideoProfile.STATE_TX_ENABLED);
+
+ mVideoCall.sendSessionModifyResponse(sessionModifyResponse);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ VideoProfile response = mConnectionInfo.mockVideoProvider
+ .getSessionModifyResponse();
+ return response != null && response.getVideoState() == VideoProfile.STATE_TX_ENABLED;
+ }
+ });
+ }
+
+ /**
+ * Tests the {@link VideoCall#requestCameraCapabilities()} ()},
+ * {@link VideoProvider#onRequestCameraCapabilities()} ()}, and
+ * {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)} APIs.
+ */
+ public void testRequestCameraCapabilities() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onCameraCapabilitiesChanged(any(CameraCapabilities.class));
+
+ mVideoCall.requestCameraCapabilities();
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onCameraCapabilitiesChanged(any(CameraCapabilities.class));
+ }
+
+ /**
+ * Tests the {@link VideoCall#setPauseImage(Uri)}, and
+ * {@link VideoProvider#onSetPauseImage(Uri)} APIs.
+ */
+ public void testSetPauseImage() throws Exception {
+ final Uri testUri = Uri.fromParts("file", "test.jpg", null);
+ mVideoCall.setPauseImage(testUri);
+
+ assertTrueWithTimeout(new Predicate<Void>() {
+ @Override
+ public boolean apply(Void v) {
+ Uri pauseImage = mConnectionInfo.mockVideoProvider.getPauseImage();
+ return pauseImage != null && pauseImage.equals(testUri);
+ }
+ });
+ }
+
+ /**
+ * Tests the {@link VideoCall#requestCallDataUsage()},
+ * {@link VideoProvider#onRequestConnectionDataUsage()}, and
+ * {@link VideoCall.Callback#onCallDataUsageChanged(long)} APIs.
+ */
+ public void testRequestDataUsage() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onCallDataUsageChanged(anyLong());
+
+ mVideoCall.requestCallDataUsage();
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onCallDataUsageChanged(eq(MockVideoProvider.DATA_USAGE));
+ }
+
+ /**
+ * Tests the {@link VideoProvider#receiveSessionModifyRequest(VideoProfile)},
+ * {@link VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile)} APIs.
+ */
+ public void testReceiveSessionModifyRequest() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onSessionModifyRequestReceived(any(VideoProfile.class));
+
+ mConnectionInfo.mockVideoProvider.sendMockSessionModifyRequest();
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ ArgumentCaptor<VideoProfile> requestProfileCaptor =
+ ArgumentCaptor.forClass(VideoProfile.class);
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onSessionModifyRequestReceived(requestProfileCaptor.capture());
+ assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+ requestProfileCaptor.getValue().getVideoState());
+ }
+
+
+ /**
+ * Tests the {@link VideoProvider#handleCallSessionEvent(int)}, and
+ * {@link VideoCall.Callback#onCallSessionEvent(int)} APIs.
+ */
+ public void testSessionEvent() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onCallSessionEvent(anyInt());
+
+ mConnectionInfo.mockVideoProvider.sendMockSessionEvent(
+ VideoProvider.SESSION_EVENT_CAMERA_READY);
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onCallSessionEvent(eq(VideoProvider.SESSION_EVENT_CAMERA_READY));
+ }
+
+ /**
+ * Tests the {@link VideoProvider#changePeerDimensions(int, int)} and
+ * {@link VideoCall.Callback#onPeerDimensionsChanged(int, int)} APIs.
+ */
+ public void testPeerDimensionChange() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onPeerDimensionsChanged(anyInt(), anyInt());
+
+ mConnectionInfo.mockVideoProvider.sendMockPeerDimensions(MockVideoProvider.PEER_DIMENSIONS,
+ MockVideoProvider.PEER_DIMENSIONS);
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onPeerDimensionsChanged(eq(MockVideoProvider.PEER_DIMENSIONS),
+ eq(MockVideoProvider.PEER_DIMENSIONS));
+ }
+
+ /**
+ * Tests the {@link VideoProvider#changeVideoQuality(int)} and
+ * {@link VideoCall.Callback#onVideoQualityChanged(int)} APIs.
+ */
+ public void testVideoQualityChange() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback)
+ .onVideoQualityChanged(anyInt());
+
+ mConnectionInfo.mockVideoProvider.sendMockVideoQuality(VideoProfile.QUALITY_HIGH);
+
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT))
+ .onVideoQualityChanged(eq(VideoProfile.QUALITY_HIGH));
+ }
+}