diff options
author | Gyanesh Mittal <gyaneshm@google.com> | 2023-06-12 21:38:32 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-06-12 21:38:32 +0000 |
commit | 770580ce49ff162a6eec3c16af415ce9603d706f (patch) | |
tree | 3b5be30a27d4b28830ab883baab0b16011f2fc96 /src | |
parent | 6cd1b0bb12053d5a725cc9db8aeb379f220820f1 (diff) | |
parent | 04cbffab6ec1f8ddc6e259212345f8cf8f4eb217 (diff) | |
download | Contacts-770580ce49ff162a6eec3c16af415ce9603d706f.tar.gz |
Merge "Add SdnProvider to AOSP Contacts app" am: d723f4f0e9 am: ffad3be5ae am: 04cbffab6e
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Contacts/+/2620851
Change-Id: I4365ca9c955e5b3ed6c154b23138e8783c4910a3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/contacts/sdn/SdnProvider.kt | 296 | ||||
-rw-r--r-- | src/com/android/contacts/sdn/SdnRepository.kt | 121 | ||||
-rw-r--r-- | src/com/android/contacts/util/PhoneNumberHelper.java | 21 |
3 files changed, 438 insertions, 0 deletions
diff --git a/src/com/android/contacts/sdn/SdnProvider.kt b/src/com/android/contacts/sdn/SdnProvider.kt new file mode 100644 index 000000000..4dd3578b1 --- /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 000000000..082adebf5 --- /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 eb070b217..2f1a5b0a0 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 @@ public class PhoneNumberHelper { } 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; + } } |