Loading Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ android_app { srcs: [ "src/**/*.java", "src/**/*.kt", "src-bind/**/*.java", ], Loading AndroidManifest.xml +11 −0 Original line number Diff line number Diff line Loading @@ -633,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 Loading res/values/donottranslate_config.xml +4 −0 Original line number Diff line number Diff line Loading @@ -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> Loading res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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 src/com/android/contacts/sdn/SdnProvider.kt 0 → 100644 +296 −0 Original line number Diff line number Diff line /* * 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 } } Loading
Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ android_app { srcs: [ "src/**/*.java", "src/**/*.kt", "src-bind/**/*.java", ], Loading
AndroidManifest.xml +11 −0 Original line number Diff line number Diff line Loading @@ -633,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 Loading
res/values/donottranslate_config.xml +4 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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
src/com/android/contacts/sdn/SdnProvider.kt 0 → 100644 +296 −0 Original line number Diff line number Diff line /* * 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 } }