1 /* <lambda>null2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.contacts.sdn 17 18 import android.content.ContentProvider 19 import android.content.ContentValues 20 import android.content.Context.TELECOM_SERVICE 21 import android.content.UriMatcher 22 import android.database.Cursor 23 import android.database.MatrixCursor 24 import android.net.Uri 25 import android.provider.ContactsContract 26 import android.provider.ContactsContract.CommonDataKinds.Phone 27 import android.provider.ContactsContract.CommonDataKinds.StructuredName 28 import android.provider.ContactsContract.Contacts 29 import android.provider.ContactsContract.Data 30 import android.provider.ContactsContract.Directory 31 import android.provider.ContactsContract.RawContacts 32 import android.telecom.TelecomManager 33 import android.util.Log 34 import com.android.contacts.R 35 36 /** Provides a way to show SDN data in search suggestions and caller id lookup. */ 37 class SdnProvider : ContentProvider() { 38 39 private lateinit var sdnRepository: SdnRepository 40 private lateinit var uriMatcher: UriMatcher 41 42 override fun onCreate(): Boolean { 43 Log.i(TAG, "onCreate") 44 val sdnProviderAuthority = requireContext().getString(R.string.contacts_sdn_provider_authority) 45 46 uriMatcher = 47 UriMatcher(UriMatcher.NO_MATCH).apply { 48 addURI(sdnProviderAuthority, "directories", DIRECTORIES) 49 addURI(sdnProviderAuthority, "contacts/filter/*", FILTER) 50 addURI(sdnProviderAuthority, "data/phones/filter/*", FILTER) 51 addURI(sdnProviderAuthority, "contacts/lookup/*/entities", CONTACT_LOOKUP) 52 addURI( 53 sdnProviderAuthority, 54 "contacts/lookup/*/#/entities", 55 CONTACT_LOOKUP_WITH_CONTACT_ID, 56 ) 57 addURI(sdnProviderAuthority, "phone_lookup/*", PHONE_LOOKUP) 58 } 59 sdnRepository = SdnRepository(requireContext()) 60 return true 61 } 62 63 override fun query( 64 uri: Uri, 65 projection: Array<out String>?, 66 selection: String?, 67 selectionArgs: Array<out String>?, 68 sortOrder: String?, 69 ): Cursor? { 70 if (projection == null) return null 71 72 val match = uriMatcher.match(uri) 73 74 if (match == DIRECTORIES) { 75 return handleDirectories(projection) 76 } 77 78 if ( 79 !isCallerAllowed(uri.getQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY)) || 80 !sdnRepository.isSdnPresent() 81 ) { 82 return null 83 } 84 85 val accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME) 86 val accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE) 87 if (ACCOUNT_NAME != accountName || ACCOUNT_TYPE != accountType) { 88 Log.e(TAG, "Received an invalid account") 89 return null 90 } 91 92 return when (match) { 93 FILTER -> handleFilter(projection, uri) 94 CONTACT_LOOKUP -> handleLookup(projection, uri.pathSegments[2]) 95 CONTACT_LOOKUP_WITH_CONTACT_ID -> 96 handleLookup(projection, uri.pathSegments[2], uri.pathSegments[3]) 97 PHONE_LOOKUP -> handlePhoneLookup(projection, uri.pathSegments[1]) 98 else -> null 99 } 100 } 101 102 override fun getType(uri: Uri) = Contacts.CONTENT_ITEM_TYPE 103 104 override fun insert(uri: Uri, values: ContentValues?): Uri? { 105 throw UnsupportedOperationException("Insert is not supported.") 106 } 107 108 override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { 109 throw UnsupportedOperationException("Delete is not supported.") 110 } 111 112 override fun update( 113 uri: Uri, 114 values: ContentValues?, 115 selection: String?, 116 selectionArgs: Array<out String>?, 117 ): Int { 118 throw UnsupportedOperationException("Update is not supported.") 119 } 120 121 private fun handleDirectories(projection: Array<out String>): Cursor { 122 // logger.atInfo().log("Creating directory cursor") 123 124 return MatrixCursor(projection).apply { 125 addRow( 126 projection.map { column -> 127 when (column) { 128 Directory.ACCOUNT_NAME -> ACCOUNT_NAME 129 Directory.ACCOUNT_TYPE -> ACCOUNT_TYPE 130 Directory.DISPLAY_NAME -> ACCOUNT_NAME 131 Directory.TYPE_RESOURCE_ID -> R.string.sdn_contacts_directory_search_label 132 Directory.EXPORT_SUPPORT -> Directory.EXPORT_SUPPORT_NONE 133 Directory.SHORTCUT_SUPPORT -> Directory.SHORTCUT_SUPPORT_NONE 134 Directory.PHOTO_SUPPORT -> Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY 135 else -> null 136 } 137 }, 138 ) 139 } 140 } 141 142 private fun handleFilter(projection: Array<out String>, uri: Uri): Cursor? { 143 val filter = uri.lastPathSegment ?: return null 144 val cursor = MatrixCursor(projection) 145 146 val results = 147 sdnRepository.fetchSdn().filter { 148 it.serviceName.contains(filter, ignoreCase = true) || it.serviceNumber.contains(filter) 149 } 150 151 if (results.isEmpty()) return cursor 152 153 val maxResult = getQueryLimit(uri) 154 155 results.take(maxResult).forEachIndexed { index, data -> 156 cursor.addRow( 157 projection.map { column -> 158 when (column) { 159 Contacts._ID -> index 160 Contacts.DISPLAY_NAME -> data.serviceName 161 Data.DATA1 -> data.serviceNumber 162 Contacts.LOOKUP_KEY -> data.lookupKey() 163 else -> null 164 } 165 }, 166 ) 167 } 168 169 return cursor 170 } 171 172 private fun handleLookup( 173 projection: Array<out String>, 174 lookupKey: String?, 175 contactIdFromUri: String? = "1", 176 ): Cursor? { 177 if (lookupKey.isNullOrEmpty()) { 178 Log.i(TAG, "handleLookup did not receive a lookup key") 179 return null 180 } 181 182 val cursor = MatrixCursor(projection) 183 val contactId = 184 try { 185 contactIdFromUri?.toLong() ?: 1L 186 } catch (_: NumberFormatException) { 187 1L 188 } 189 190 val result = sdnRepository.fetchSdn().find { it.lookupKey() == lookupKey } ?: return cursor 191 192 // Adding first row for name 193 cursor.addRow( 194 projection.map { column -> 195 when (column) { 196 Contacts.Entity.CONTACT_ID -> contactId 197 Contacts.Entity.RAW_CONTACT_ID -> contactId 198 Contacts.Entity.DATA_ID -> 1 199 Data.MIMETYPE -> StructuredName.CONTENT_ITEM_TYPE 200 StructuredName.DISPLAY_NAME -> result.serviceName 201 StructuredName.GIVEN_NAME -> result.serviceName 202 Contacts.DISPLAY_NAME -> result.serviceName 203 Contacts.DISPLAY_NAME_ALTERNATIVE -> result.serviceName 204 RawContacts.ACCOUNT_NAME -> ACCOUNT_NAME 205 RawContacts.ACCOUNT_TYPE -> ACCOUNT_TYPE 206 RawContacts.RAW_CONTACT_IS_READ_ONLY -> 1 207 Contacts.LOOKUP_KEY -> result.lookupKey() 208 else -> null 209 } 210 } 211 ) 212 213 // Adding second row for number 214 cursor.addRow( 215 projection.map { column -> 216 when (column) { 217 Contacts.Entity.CONTACT_ID -> contactId 218 Contacts.Entity.RAW_CONTACT_ID -> contactId 219 Contacts.Entity.DATA_ID -> 2 220 Data.MIMETYPE -> Phone.CONTENT_ITEM_TYPE 221 Phone.NUMBER -> result.serviceNumber 222 Data.IS_PRIMARY -> 1 223 Phone.TYPE -> Phone.TYPE_MAIN 224 else -> null 225 } 226 } 227 ) 228 229 return cursor 230 } 231 232 private fun handlePhoneLookup( 233 projection: Array<out String>, 234 phoneNumber: String?, 235 ): Cursor? { 236 if (phoneNumber.isNullOrEmpty()) { 237 Log.i(TAG, "handlePhoneLookup did not receive a phoneNumber") 238 return null 239 } 240 241 val cursor = MatrixCursor(projection) 242 243 val result = sdnRepository.fetchSdn().find { it.serviceNumber == phoneNumber } ?: return cursor 244 245 cursor.addRow( 246 projection.map { column -> 247 when (column) { 248 Contacts.DISPLAY_NAME -> result.serviceName 249 Phone.NUMBER -> result.serviceNumber 250 else -> null 251 } 252 }, 253 ) 254 255 return cursor 256 } 257 258 private fun isCallerAllowed(callingPackage: String?): Boolean { 259 if (callingPackage.isNullOrEmpty()) { 260 Log.i(TAG, "Calling package is null or empty.") 261 return false 262 } 263 264 if (callingPackage == requireContext().packageName) { 265 return true 266 } 267 268 // Check if the calling package is default dialer app or not 269 val context = context ?: return false 270 val tm = context.getSystemService(TELECOM_SERVICE) as TelecomManager 271 return tm.defaultDialerPackage == callingPackage 272 } 273 274 private fun getQueryLimit(uri: Uri): Int { 275 return try { 276 uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY)?.toInt() ?: DEFAULT_MAX_RESULTS 277 } catch (e: NumberFormatException) { 278 DEFAULT_MAX_RESULTS 279 } 280 } 281 282 companion object { 283 private val TAG = SdnProvider::class.java.simpleName 284 285 private const val DIRECTORIES = 0 286 private const val FILTER = 1 287 private const val CONTACT_LOOKUP = 2 288 private const val CONTACT_LOOKUP_WITH_CONTACT_ID = 3 289 private const val PHONE_LOOKUP = 4 290 291 private const val ACCOUNT_NAME = "Carrier service numbers" 292 private const val ACCOUNT_TYPE = "com.android.contacts.sdn" 293 294 private const val DEFAULT_MAX_RESULTS = 20 295 } 296 } 297