summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGyanesh Mittal <gyaneshm@google.com>2023-06-12 21:38:32 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-06-12 21:38:32 +0000
commit770580ce49ff162a6eec3c16af415ce9603d706f (patch)
tree3b5be30a27d4b28830ab883baab0b16011f2fc96 /src
parent6cd1b0bb12053d5a725cc9db8aeb379f220820f1 (diff)
parent04cbffab6ec1f8ddc6e259212345f8cf8f4eb217 (diff)
downloadContacts-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.kt296
-rw-r--r--src/com/android/contacts/sdn/SdnRepository.kt121
-rw-r--r--src/com/android/contacts/util/PhoneNumberHelper.java21
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;
+ }
}