[automerger skipped] Merge "Import translations. DO NOT MERGE ANYWHERE" into udc-dev am: 8633f532e9 -s ours am: b7f00f5562 -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Contacts/+/23554944

Change-Id: I05d6c7159b3a747c92ffb017fa5788924d2834ac
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index de90412..36923ab 100644
--- a/Android.bp
+++ b/Android.bp
@@ -25,6 +25,7 @@
 
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "src-bind/**/*.java",
     ],
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0bd96cd..66d5767 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,14 +20,15 @@
           android:versionName="1.7.34">
 
     <uses-sdk
-        android:minSdkVersion="33"
-        android:targetSdkVersion="33"/>
+        android:minSdkVersion="34"
+        android:targetSdkVersion="34"/>
 
     <original-package android:name="com.android.contacts"/>
 
     <!-- Contacts permission listed first because order determines
          order that permissions are requested with some OEMs -->
     <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
     <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
     <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED"/>
@@ -56,6 +57,8 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <!-- Required in U to run Service.startForeground() under DATA_SYNC type -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
 
     <uses-feature
         android:name="android.hardware.telephony"
@@ -630,6 +633,17 @@
             android:name="android.nfc.disable_beam_default"
             android:value="true"/>
 
+        <provider
+            android:name="com.android.contacts.sdn.SdnProvider"
+            android:authorities="@string/contacts_sdn_provider_authority"
+            android:enabled="true"
+            android:exported="true"
+            android:readPermission="android.permission.BIND_DIRECTORY_SEARCH">
+            <meta-data
+                android:name="android.content.ContactDirectory"
+                android:value="true" />
+        </provider>
+
     </application>
 
     <!-- Allows the contacts app to see the activities and services needed
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 13c674d..18c1684 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -91,8 +91,8 @@
     <string name="customLabelPickerTitle" msgid="816694850254307154">"উপযোগিতা অনুসৰি লেবেলৰ নাম"</string>
     <string name="removePhoto" msgid="1190099414600730001">"ফট\' আঁতৰাওক"</string>
     <string name="noContacts" msgid="3030512741779213810">"আপোনাৰ সম্পৰ্কসূচী খালী আছে"</string>
-    <string name="emptyGroup" msgid="8598261660865081152">"এই লেবেলত কোনো সম্পৰ্ক ব্য়ক্তি নাই"</string>
-    <string name="emptyAccount" msgid="7450843210977018582">"এই একাউণ্টত কোনো সম্পৰ্ক ব্য়ক্তি নাই"</string>
+    <string name="emptyGroup" msgid="8598261660865081152">"এই লেবেলত কোনো সম্পৰ্ক ব্যক্তি নাই"</string>
+    <string name="emptyAccount" msgid="7450843210977018582">"এই একাউণ্টত কোনো সম্পৰ্ক ব্যক্তি নাই"</string>
     <string name="emptyMainList" msgid="3266182207039677163">"আপোনাৰ সম্পৰ্কসূচী খালী আছে"</string>
     <string name="contactSavedNamedToast" msgid="3067050290584834386">"<xliff:g id="DISPLAY_NAME">%s</xliff:g> ছেভ কৰা হ’ল"</string>
     <string name="contactSavedToast" msgid="4370392215196995301">"সম্পৰ্ক ছেভ কৰা হ’ল"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 0dc36e0..3810199 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -331,7 +331,7 @@
     <string name="postal_region" msgid="6130239447563491435">"Πολιτεία"</string>
     <string name="postal_postcode" msgid="33077708757232659">"Ταχυδρομικός κώδικας"</string>
     <string name="postal_country" msgid="6642804283917549861">"Χώρα"</string>
-    <string name="map_home" msgid="2169053372466501148">"Προβολή διεύθυνσης οικίας"</string>
+    <string name="map_home" msgid="2169053372466501148">"Προβολή διεύθυνσης σπιτιού"</string>
     <string name="map_work" msgid="8296916987749726461">"Προβολή διεύθυνσης εργασίας"</string>
     <string name="map_other" msgid="4009931029322619674">"Προβολή διεύθυνσης"</string>
     <string name="map_custom" msgid="7797812861927817335">"Προβολή διεύθυνσης <xliff:g id="CUSTOM_LABEL">%s</xliff:g>"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 4146e71..6eefd87 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -193,7 +193,7 @@
     <string name="expanding_entry_card_view_see_less" msgid="6399603072579278030">"ಕಡಿಮೆ ನೋಡಿ"</string>
     <string name="about_card_title" msgid="6635849009952435700">"ಕುರಿತು"</string>
     <string name="toast_making_personal_copy" msgid="9053129410039312386">"ವೈಯಕ್ತಿಕ ಪ್ರತಿಯನ್ನು ರಚಿಸಲಾಗುತ್ತಿದೆ…"</string>
-    <string name="date_time_set" msgid="8526160894146496334">"ಹೊಂದಿಸಿ"</string>
+    <string name="date_time_set" msgid="8526160894146496334">"ಸೆಟ್ ಮಾಡಿ"</string>
     <string name="header_im_entry" msgid="3581797653862294826">"IM"</string>
     <string name="header_organization_entry" msgid="7428066442988227441">"ಸಂಸ್ಥೆ"</string>
     <string name="header_nickname_entry" msgid="1110276804512795150">"ಅಡ್ಡಹೆಸರು"</string>
@@ -387,7 +387,7 @@
     <string name="fail_reason_not_supported" msgid="6449916670421646290">"ಈ ಫಾರ್ಮ್ಯಾಟ್ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ."</string>
     <string name="exporting_vcard_finished_title" msgid="1984393609140969504">"<xliff:g id="FILENAME">%s</xliff:g> ರಫ್ತು ಮುಗಿದಿದೆ."</string>
     <string name="exporting_vcard_finished_title_fallback" msgid="9029067439586573959">"ಸಂಪರ್ಕಗಳ ರಫ್ತು ಮಾಡುವಿಕೆ ಮುಗಿದಿದೆ."</string>
-    <string name="exporting_vcard_finished_toast" msgid="5463125514187187782">"ಸಂಪರ್ಕಗಳನ್ನು ರಪ್ತು ಮಾಡುವುದನ್ನು ಮುಕ್ತಾಯಗೊಳಿಸಲಾಗಿದೆ, ಸಂಪರ್ಕಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಅಧಿಸೂಚನೆ ಕ್ಲಿಕ್ ಮಾಡಿ."</string>
+    <string name="exporting_vcard_finished_toast" msgid="5463125514187187782">"ಸಂಪರ್ಕಗಳನ್ನು ರಪ್ತು ಮಾಡುವುದನ್ನು ಮುಕ್ತಾಯಗೊಳಿಸಲಾಗಿದೆ, ಸಂಪರ್ಕಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ನೋಟಿಫಿಕೇಶನ್ ಕ್ಲಿಕ್ ಮಾಡಿ."</string>
     <string name="touch_to_share_contacts" msgid="7678194978416052577">"ಸಂಪರ್ಕಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
     <string name="exporting_vcard_canceled_title" msgid="1287529222628052526">"<xliff:g id="FILENAME">%s</xliff:g> ರಫ್ತು ಮಾಡುವುದನ್ನು ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ."</string>
     <string name="exporting_contact_list_title" msgid="6599904516394311592">"ಸಂಪರ್ಕ ಡೇಟಾವನ್ನು ರಫ್ತುಮಾಡಲಾಗುತ್ತಿದೆ"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 0b3c5f4..d623661 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -79,7 +79,7 @@
     <string name="single_delete_confirmation" msgid="8260949300855537648">"Бул байланыш жок кылынсынбы?"</string>
     <string name="batch_delete_confirmation" msgid="4149615167210863403">"Тандалган байланыштар жок кылынсынбы?"</string>
     <string name="batch_delete_read_only_contact_confirmation" msgid="381691735715182700">"Окуу үчүн гана арналган аккаунтуңуздан байланыштарды жок кылуу мүмкүн эмес, бирок аларды жашырып койсо болот."</string>
-    <string name="batch_delete_multiple_accounts_confirmation" msgid="4547718538924570984">"Жок кылына турган байланыштарда бир нече аккаунтунун чоо-жайы бар. Окуу үчүн гана арналган аккаунттарынын чоо-жайы жашырылып, өчпөйт."</string>
+    <string name="batch_delete_multiple_accounts_confirmation" msgid="4547718538924570984">"Өчүрүлө турган байланыштарда бир нече аккаунтунун чоо-жайы бар. Окуу үчүн гана арналган аккаунттарынын чоо-жайы жашырылып, өчпөйт."</string>
     <string name="multipleContactDeleteConfirmation" msgid="2970218685653877287">"Бул байланыш өчүрүлсө, анын бир нече аккаунтундагы чоо-жайы өчүп калат."</string>
     <string name="deleteConfirmation" msgid="3727809366015979585">"Бул байланыш жок кылынсынбы?"</string>
     <string name="deleteConfirmation_positive_button" msgid="1604511403421785160">"Жок кылуу"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 9bf3593..855fdee 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -221,8 +221,8 @@
     <string name="editor_delete_view_description_short" msgid="6471032197042328181">"<xliff:g id="DATA_KIND">%s</xliff:g> ਹਟਾਓ"</string>
     <string name="menu_assistant" msgid="3980364519724237934">"ਸੁਝਾਅ"</string>
     <string name="menu_assistant_new_badge" msgid="1992049406019853898">"ਨਵਾਂ"</string>
-    <string name="navigation_drawer_open" msgid="3428713515765555712">"ਆਵਾਗੌਣ ਦਰਾਜ਼ ਖੋਲ੍ਹੋ"</string>
-    <string name="navigation_drawer_close" msgid="3052989100471689722">"ਆਵਾਗੌਣ ਦਰਾਜ਼ ਬੰਦ ਕਰੋ"</string>
+    <string name="navigation_drawer_open" msgid="3428713515765555712">"ਨੈਵੀਗੇਸ਼ਨ ਡ੍ਰਾਅਰ ਖੋਲ੍ਹੋ"</string>
+    <string name="navigation_drawer_close" msgid="3052989100471689722">"ਨੈਵੀਗੇਸ਼ਨ ਡ੍ਰਾਅਰ ਬੰਦ ਕਰੋ"</string>
     <string name="navigation_drawer_label" msgid="8391863484838783638">"<xliff:g id="LABEL_NAME">%s</xliff:g> ਲੇਬਲ"</string>
     <string name="menu_title_groups" msgid="3722199658759568221">"ਲੇਬਲ"</string>
     <string name="menu_title_filters" msgid="349866121417914494">"ਖਾਤੇ"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index f596105..398f2d7 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -310,7 +310,7 @@
     <string name="organizationLabelsGroup" msgid="2342482097897299099">"సంస్థ"</string>
     <string name="relationLabelsGroup" msgid="8931615792208307291">"సంబంధం"</string>
     <string name="eventLabelsGroup" msgid="8625868552164376823">"ప్రత్యేక తేదీ"</string>
-    <string name="sms" msgid="4246338112764847384">"వచన మెసేజ్‌"</string>
+    <string name="sms" msgid="4246338112764847384">"టెక్స్ట్ మెసేజ్‌"</string>
     <string name="postal_address" msgid="5031809899673855074">"అడ్రస్‌"</string>
     <string name="ghostData_company" msgid="3873500610390675876">"కంపెనీ"</string>
     <string name="ghostData_department" msgid="8610642449404163799">"విభాగం"</string>
diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml
index 2dd0e73..c4c0152 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -28,6 +28,10 @@
 
     <!-- File Authority for AOSP Contacts files -->
     <string name="contacts_file_provider_authority">com.android.contacts.files</string>
+
+    <!-- SDN Authority for carrier SDN -->
+    <string name="contacts_sdn_provider_authority">com.android.contacts.sdn</string>
+
     <!-- Flag indicating whether Contacts app is allowed to import contacts -->
     <bool name="config_allow_import_from_vcf_file">true</bool>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d87be0b..b608e12 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1549,4 +1549,7 @@
     <!-- Text of Negative Button in dialog -->
     <string name="no_button">No</string>
 
+    <!-- The label to display as a section header in the contact list for an SDN directory [CHAR LIMIT=40] -->
+    <string name="sdn_contacts_directory_search_label">Carrier service numbers</string>
+
 </resources>
\ No newline at end of file
diff --git a/src/com/android/contacts/activities/RequestPermissionsActivity.java b/src/com/android/contacts/activities/RequestPermissionsActivity.java
index ebcd9b3..7994e98 100644
--- a/src/com/android/contacts/activities/RequestPermissionsActivity.java
+++ b/src/com/android/contacts/activities/RequestPermissionsActivity.java
@@ -70,6 +70,7 @@
                 permissions.add(permission.CALL_PHONE);
                 permissions.add(permission.READ_PHONE_NUMBERS);
                 permissions.add(permission.READ_PHONE_STATE);
+                permissions.add(permission.READ_CALL_LOG);
             }
             sRequiredPermissions = permissions.toArray(new String[0]);
         }
diff --git a/src/com/android/contacts/sdn/SdnProvider.kt b/src/com/android/contacts/sdn/SdnProvider.kt
new file mode 100644
index 0000000..4dd3578
--- /dev/null
+++ b/src/com/android/contacts/sdn/SdnProvider.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.sdn
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context.TELECOM_SERVICE
+import android.content.UriMatcher
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.provider.ContactsContract
+import android.provider.ContactsContract.CommonDataKinds.Phone
+import android.provider.ContactsContract.CommonDataKinds.StructuredName
+import android.provider.ContactsContract.Contacts
+import android.provider.ContactsContract.Data
+import android.provider.ContactsContract.Directory
+import android.provider.ContactsContract.RawContacts
+import android.telecom.TelecomManager
+import android.util.Log
+import com.android.contacts.R
+
+/** Provides a way to show SDN data in search suggestions and caller id lookup. */
+class SdnProvider : ContentProvider() {
+
+  private lateinit var sdnRepository: SdnRepository
+  private lateinit var uriMatcher: UriMatcher
+
+  override fun onCreate(): Boolean {
+    Log.i(TAG, "onCreate")
+    val sdnProviderAuthority = requireContext().getString(R.string.contacts_sdn_provider_authority)
+
+    uriMatcher =
+      UriMatcher(UriMatcher.NO_MATCH).apply {
+        addURI(sdnProviderAuthority, "directories", DIRECTORIES)
+        addURI(sdnProviderAuthority, "contacts/filter/*", FILTER)
+        addURI(sdnProviderAuthority, "data/phones/filter/*", FILTER)
+        addURI(sdnProviderAuthority, "contacts/lookup/*/entities", CONTACT_LOOKUP)
+        addURI(
+          sdnProviderAuthority,
+          "contacts/lookup/*/#/entities",
+          CONTACT_LOOKUP_WITH_CONTACT_ID,
+        )
+        addURI(sdnProviderAuthority, "phone_lookup/*", PHONE_LOOKUP)
+      }
+    sdnRepository = SdnRepository(requireContext())
+    return true
+  }
+
+  override fun query(
+    uri: Uri,
+    projection: Array<out String>?,
+    selection: String?,
+    selectionArgs: Array<out String>?,
+    sortOrder: String?,
+  ): Cursor? {
+    if (projection == null) return null
+
+    val match = uriMatcher.match(uri)
+
+    if (match == DIRECTORIES) {
+      return handleDirectories(projection)
+    }
+
+    if (
+      !isCallerAllowed(uri.getQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY)) ||
+      !sdnRepository.isSdnPresent()
+    ) {
+      return null
+    }
+
+    val accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME)
+    val accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE)
+    if (ACCOUNT_NAME != accountName || ACCOUNT_TYPE != accountType) {
+      Log.e(TAG, "Received an invalid account")
+      return null
+    }
+
+    return when (match) {
+      FILTER -> handleFilter(projection, uri)
+      CONTACT_LOOKUP -> handleLookup(projection, uri.pathSegments[2])
+      CONTACT_LOOKUP_WITH_CONTACT_ID ->
+        handleLookup(projection, uri.pathSegments[2], uri.pathSegments[3])
+      PHONE_LOOKUP -> handlePhoneLookup(projection, uri.pathSegments[1])
+      else -> null
+    }
+  }
+
+  override fun getType(uri: Uri) = Contacts.CONTENT_ITEM_TYPE
+
+  override fun insert(uri: Uri, values: ContentValues?): Uri? {
+    throw UnsupportedOperationException("Insert is not supported.")
+  }
+
+  override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
+    throw UnsupportedOperationException("Delete is not supported.")
+  }
+
+  override fun update(
+    uri: Uri,
+    values: ContentValues?,
+    selection: String?,
+    selectionArgs: Array<out String>?,
+  ): Int {
+    throw UnsupportedOperationException("Update is not supported.")
+  }
+
+  private fun handleDirectories(projection: Array<out String>): Cursor {
+    // logger.atInfo().log("Creating directory cursor")
+
+    return MatrixCursor(projection).apply {
+      addRow(
+        projection.map { column ->
+          when (column) {
+            Directory.ACCOUNT_NAME -> ACCOUNT_NAME
+            Directory.ACCOUNT_TYPE -> ACCOUNT_TYPE
+            Directory.DISPLAY_NAME -> ACCOUNT_NAME
+            Directory.TYPE_RESOURCE_ID -> R.string.sdn_contacts_directory_search_label
+            Directory.EXPORT_SUPPORT -> Directory.EXPORT_SUPPORT_NONE
+            Directory.SHORTCUT_SUPPORT -> Directory.SHORTCUT_SUPPORT_NONE
+            Directory.PHOTO_SUPPORT -> Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
+            else -> null
+          }
+        },
+      )
+    }
+  }
+
+  private fun handleFilter(projection: Array<out String>, uri: Uri): Cursor? {
+    val filter = uri.lastPathSegment ?: return null
+    val cursor = MatrixCursor(projection)
+
+    val results =
+      sdnRepository.fetchSdn().filter {
+        it.serviceName.contains(filter, ignoreCase = true) || it.serviceNumber.contains(filter)
+      }
+
+    if (results.isEmpty()) return cursor
+
+    val maxResult = getQueryLimit(uri)
+
+    results.take(maxResult).forEachIndexed { index, data ->
+      cursor.addRow(
+        projection.map { column ->
+          when (column) {
+            Contacts._ID -> index
+            Contacts.DISPLAY_NAME -> data.serviceName
+            Data.DATA1 -> data.serviceNumber
+            Contacts.LOOKUP_KEY -> data.lookupKey()
+            else -> null
+          }
+        },
+      )
+    }
+
+    return cursor
+  }
+
+  private fun handleLookup(
+    projection: Array<out String>,
+    lookupKey: String?,
+    contactIdFromUri: String? = "1",
+  ): Cursor? {
+    if (lookupKey.isNullOrEmpty()) {
+      Log.i(TAG, "handleLookup did not receive a lookup key")
+      return null
+    }
+
+    val cursor = MatrixCursor(projection)
+    val contactId =
+      try {
+        contactIdFromUri?.toLong() ?: 1L
+      } catch (_: NumberFormatException) {
+        1L
+      }
+
+    val result = sdnRepository.fetchSdn().find { it.lookupKey() == lookupKey } ?: return cursor
+
+    // Adding first row for name
+    cursor.addRow(
+      projection.map { column ->
+        when (column) {
+          Contacts.Entity.CONTACT_ID -> contactId
+          Contacts.Entity.RAW_CONTACT_ID -> contactId
+          Contacts.Entity.DATA_ID -> 1
+          Data.MIMETYPE -> StructuredName.CONTENT_ITEM_TYPE
+          StructuredName.DISPLAY_NAME -> result.serviceName
+          StructuredName.GIVEN_NAME -> result.serviceName
+          Contacts.DISPLAY_NAME -> result.serviceName
+          Contacts.DISPLAY_NAME_ALTERNATIVE -> result.serviceName
+          RawContacts.ACCOUNT_NAME -> ACCOUNT_NAME
+          RawContacts.ACCOUNT_TYPE -> ACCOUNT_TYPE
+          RawContacts.RAW_CONTACT_IS_READ_ONLY -> 1
+          Contacts.LOOKUP_KEY -> result.lookupKey()
+          else -> null
+        }
+      }
+    )
+
+    // Adding second row for number
+    cursor.addRow(
+      projection.map { column ->
+        when (column) {
+          Contacts.Entity.CONTACT_ID -> contactId
+          Contacts.Entity.RAW_CONTACT_ID -> contactId
+          Contacts.Entity.DATA_ID -> 2
+          Data.MIMETYPE -> Phone.CONTENT_ITEM_TYPE
+          Phone.NUMBER -> result.serviceNumber
+          Data.IS_PRIMARY -> 1
+          Phone.TYPE -> Phone.TYPE_MAIN
+          else -> null
+        }
+      }
+    )
+
+    return cursor
+  }
+
+  private fun handlePhoneLookup(
+    projection: Array<out String>,
+    phoneNumber: String?,
+  ): Cursor? {
+    if (phoneNumber.isNullOrEmpty()) {
+      Log.i(TAG, "handlePhoneLookup did not receive a phoneNumber")
+      return null
+    }
+
+    val cursor = MatrixCursor(projection)
+
+    val result = sdnRepository.fetchSdn().find { it.serviceNumber == phoneNumber } ?: return cursor
+
+    cursor.addRow(
+      projection.map { column ->
+        when (column) {
+          Contacts.DISPLAY_NAME -> result.serviceName
+          Phone.NUMBER -> result.serviceNumber
+          else -> null
+        }
+      },
+    )
+
+    return cursor
+  }
+
+  private fun isCallerAllowed(callingPackage: String?): Boolean {
+    if (callingPackage.isNullOrEmpty()) {
+      Log.i(TAG, "Calling package is null or empty.")
+      return false
+    }
+
+    if (callingPackage == requireContext().packageName) {
+      return true
+    }
+
+    // Check if the calling package is default dialer app or not
+    val context = context ?: return false
+    val tm = context.getSystemService(TELECOM_SERVICE) as TelecomManager
+    return tm.defaultDialerPackage == callingPackage
+  }
+
+  private fun getQueryLimit(uri: Uri): Int {
+    return try {
+      uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY)?.toInt() ?: DEFAULT_MAX_RESULTS
+    } catch (e: NumberFormatException) {
+      DEFAULT_MAX_RESULTS
+    }
+  }
+
+  companion object {
+    private val TAG = SdnProvider::class.java.simpleName
+
+    private const val DIRECTORIES = 0
+    private const val FILTER = 1
+    private const val CONTACT_LOOKUP = 2
+    private const val CONTACT_LOOKUP_WITH_CONTACT_ID = 3
+    private const val PHONE_LOOKUP = 4
+
+    private const val ACCOUNT_NAME = "Carrier service numbers"
+    private const val ACCOUNT_TYPE = "com.android.contacts.sdn"
+
+    private const val DEFAULT_MAX_RESULTS = 20
+  }
+}
diff --git a/src/com/android/contacts/sdn/SdnRepository.kt b/src/com/android/contacts/sdn/SdnRepository.kt
new file mode 100644
index 0000000..082adeb
--- /dev/null
+++ b/src/com/android/contacts/sdn/SdnRepository.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.sdn
+
+import android.Manifest.permission
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
+import android.util.Log
+import com.android.contacts.model.SimCard
+import com.android.contacts.util.PermissionsUtil
+import com.android.contacts.util.PhoneNumberHelper
+
+/** Repository to fetch Sdn data from [CarrierConfigManager]. */
+class SdnRepository constructor(private val context: Context) {
+
+  fun isSdnPresent(): Boolean {
+    if (
+      !hasTelephony() ||
+      !PermissionsUtil.hasPermission(context, permission.READ_PHONE_STATE) ||
+      !PermissionsUtil.hasPermission(context, permission.READ_PHONE_NUMBERS) ||
+      !PermissionsUtil.hasPermission(context, permission.READ_CALL_LOG)
+    ) {
+      return false
+    }
+
+    val simCardList = getSimCardInformation()
+
+    for (simCard in simCardList) {
+      if (fetchSdnFromCarrierConfig(simCard).isNotEmpty()) {
+        Log.i(TAG, "Found SDN list from CarrierConfig")
+        return true
+      }
+    }
+    return false
+  }
+
+  fun fetchSdn(): List<Sdn> {
+    val simCardList = getSimCardInformation()
+
+    return simCardList
+      .flatMap { fetchSdnFromCarrierConfig(it) }
+      .distinct()
+      .sortedBy { it.serviceName }
+  }
+
+  // Permission check isn't recognized by the linter.
+  @SuppressLint("MissingPermission")
+  fun getSimCardInformation(): List<SimCard> {
+    val subscriptionManager = context.getSystemService(SubscriptionManager::class.java)
+    return subscriptionManager.activeSubscriptionInfoList?.filterNotNull()?.mapNotNull {
+      if (it.subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+        null
+      } else {
+        SimCard.create(it)
+      }
+    }
+      ?: emptyList()
+  }
+
+  @Suppress("Deprecation", "MissingPermission")
+  private fun fetchSdnFromCarrierConfig(simCard: SimCard): List<Sdn> {
+    val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)
+    val carrierConfig =
+      carrierConfigManager.getConfigForSubId(simCard.subscriptionId) ?: return emptyList()
+    val nameList: List<String> =
+      carrierConfig
+        .getStringArray(CarrierConfigManager.KEY_CARRIER_SERVICE_NAME_STRING_ARRAY)
+        ?.map { it?.trim() ?: "" }
+        ?: return emptyList()
+    val numberList: List<String> =
+      carrierConfig
+        .getStringArray(CarrierConfigManager.KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY)
+        ?.map { it?.trim() ?: "" }
+        ?: return emptyList()
+    if (nameList.isEmpty() || nameList.size != numberList.size) return emptyList()
+
+    val sdnList = mutableListOf<Sdn>()
+    nameList.zip(numberList).forEach { (sdnServiceName, sdnNumber) ->
+      if (sdnServiceName.isNotBlank() && PhoneNumberHelper.isDialablePhoneNumber(sdnNumber)) {
+        sdnList.add(Sdn(sdnServiceName, sdnNumber))
+      }
+    }
+    return sdnList
+  }
+
+  private fun hasTelephony(): Boolean {
+    return context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+  }
+
+  companion object {
+    private val TAG = SdnRepository::class.java.simpleName
+  }
+}
+
+/** Hold the Service dialing number information to be displayed in SdnActivity. */
+data class Sdn(
+  val serviceName: String,
+  val serviceNumber: String,
+) {
+
+  /** Generate lookup key that will help identify SDN when Opening QuickContact. */
+  fun lookupKey(): String {
+    return "non-sim-sdn-" + hashCode()
+  }
+}
diff --git a/src/com/android/contacts/util/PhoneNumberHelper.java b/src/com/android/contacts/util/PhoneNumberHelper.java
index eb070b2..2f1a5b0 100644
--- a/src/com/android/contacts/util/PhoneNumberHelper.java
+++ b/src/com/android/contacts/util/PhoneNumberHelper.java
@@ -16,6 +16,7 @@
 package com.android.contacts.util;
 
 import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
 import android.util.Log;
 
 /**
@@ -95,4 +96,24 @@
         }
         return number.substring(0, delimiterIndex);
     }
+
+    /** Returns true if the given string is dialable by the user from Phone/Dialer app. */
+    public static boolean isDialablePhoneNumber(String str) {
+        if (TextUtils.isEmpty(str)) {
+            return false;
+        }
+
+        for (int i = 0, count = str.length(); i < count; i++) {
+            if (!(PhoneNumberUtils.isDialable(str.charAt(i))
+                || str.charAt(i) == ' '
+                || str.charAt(i) == '-'
+                || str.charAt(i) == '('
+                || str.charAt(i) == ')'
+                || str.charAt(i) == '.'
+                || str.charAt(i) == '/')) {
+                return false;
+            }
+        }
+        return true;
+    }
 }
diff --git a/tests/Android.bp b/tests/Android.bp
index 647fee7..2912673 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -29,7 +29,7 @@
         "androidx.test.ext.junit",
         "hamcrest-library",
         "mockito-target-minus-junit4",
-        "ub-uiautomator",
+        "androidx.test.uiautomator_uiautomator",
     ],
 
     libs: [
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index f22f92c..9ccfa3f 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -20,6 +20,7 @@
     <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
 
     <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
 
diff --git a/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
index fac9c85..59f8147 100644
--- a/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
+++ b/tests/src/com/android/contacts/NoPermissionsLaunchSmokeTest.java
@@ -8,15 +8,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.Suppress;
 import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -54,6 +54,7 @@
         assumeTrue(!hasPermission(mTargetContext, Manifest.permission.GET_ACCOUNTS));
         assumeTrue(!hasPermission(mTargetContext, Manifest.permission.READ_PHONE_STATE));
         assumeTrue(!hasPermission(mTargetContext, Manifest.permission.CALL_PHONE));
+        assumeTrue(!hasPermission(mTargetContext, Manifest.permission.READ_CALL_LOG));
 
         // remove state that might exist outside of the app
         // (e.g. launcher shortcuts and scheduled jobs)
@@ -72,14 +73,20 @@
 
         device.waitForIdle();
 
-        device.wait(Until.hasObject(By.textStartsWith("Allow Contacts")), TIMEOUT);
+        final PackageManager packageManager = mTargetContext.getPackageManager();
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            device.wait(Until.hasObject(By.textEndsWith("your phone call logs?")), TIMEOUT);
+            final UiObject2 grantCallLogPermissionButton = device.findObject(By.text("ALLOW"));
+            grantCallLogPermissionButton.click();
+        }
+
+        device.wait(Until.hasObject(By.textEndsWith("access your contacts?")), TIMEOUT);
         final UiObject2 grantContactsPermissionButton = device.findObject(By.text("ALLOW"));
 
         grantContactsPermissionButton.click();
 
         device.wait(Until.hasObject(By.textEndsWith("make and manage phone calls?")), TIMEOUT);
 
-        final PackageManager packageManager = mTargetContext.getPackageManager();
         if (!packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
             device.waitForIdle();
             return;
diff --git a/tests/src/com/android/contacts/activities/SimImportActivityTest.java b/tests/src/com/android/contacts/activities/SimImportActivityTest.java
index 250ce4b..9e2f73b 100644
--- a/tests/src/com/android/contacts/activities/SimImportActivityTest.java
+++ b/tests/src/com/android/contacts/activities/SimImportActivityTest.java
@@ -42,9 +42,6 @@
 import android.provider.ContactsContract.Data;
 import android.provider.SimPhonebookContract;
 import android.provider.SimPhonebookContract.SimRecords;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.Until;
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
 
@@ -53,6 +50,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
 
 import com.android.contacts.SimImportService;
 import com.android.contacts.database.SimContactDao;