Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d723f4f0 authored by Gyanesh Mittal's avatar Gyanesh Mittal Committed by Gerrit Code Review
Browse files

Merge "Add SdnProvider to AOSP Contacts app"

parents b3510290 d3db7dca
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ android_app {

    srcs: [
        "src/**/*.java",
        "src/**/*.kt",
        "src-bind/**/*.java",
    ],

+11 −0
Original line number Diff line number Diff line
@@ -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
+4 −0
Original line number Diff line number Diff line
@@ -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>

+3 −0
Original line number Diff line number Diff line
@@ -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
+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