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