/* * Copyright (C) 2020 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 android.provider; import static android.provider.SimPhonebookContract.ElementaryFiles.EF_ADN; import static android.provider.SimPhonebookContract.ElementaryFiles.EF_FDN; import static android.provider.SimPhonebookContract.ElementaryFiles.EF_SDN; import static android.provider.SimPhonebookContract.ElementaryFiles.PATH_SEGMENT_EF_ADN; import static android.provider.SimPhonebookContract.ElementaryFiles.PATH_SEGMENT_EF_FDN; import static android.provider.SimPhonebookContract.ElementaryFiles.PATH_SEGMENT_EF_SDN; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.WorkerThread; import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; import android.os.Bundle; import android.telephony.SubscriptionInfo; import android.telephony.TelephonyManager; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** * The contract between the provider of contact records on the device's SIM cards and applications. * Contains definitions of the supported URIs and columns. * *

Permissions

*

* Querying this provider requires {@link android.Manifest.permission#READ_CONTACTS} and writing * to this provider requires {@link android.Manifest.permission#WRITE_CONTACTS} *

*/ public final class SimPhonebookContract { /** The authority for the SIM phonebook provider. */ public static final String AUTHORITY = "com.android.simphonebook"; /** The content:// style uri to the authority for the SIM phonebook provider. */ @NonNull public static final Uri AUTHORITY_URI = Uri.parse("content://com.android.simphonebook"); /** * The Uri path element used to indicate that the following path segment is a subscription ID * for the SIM card that will be operated on. * * @hide */ public static final String SUBSCRIPTION_ID_PATH_SEGMENT = "subid"; private SimPhonebookContract() { } /** * Returns the Uri path segment used to reference the specified elementary file type for Uris * returned by this API. * * @hide */ @NonNull public static String getEfUriPath(@ElementaryFiles.EfType int efType) { switch (efType) { case EF_ADN: return PATH_SEGMENT_EF_ADN; case EF_FDN: return PATH_SEGMENT_EF_FDN; case EF_SDN: return PATH_SEGMENT_EF_SDN; default: throw new IllegalArgumentException("Unsupported EfType " + efType); } } /** * Constants for the contact records on a SIM card. * *

Data

*

* Data is stored in a specific elementary file on a specific SIM card and these are isolated * from each other. SIM cards are identified by their subscription ID. SIM cards may not support * all or even any of the elementary file types. A SIM will have constraints on * the values of the data that can be stored in each elementary file. The available SIMs, * their supported elementary file types and the constraints on the data can be discovered by * querying {@link ElementaryFiles#CONTENT_URI}. Each elementary file has a fixed capacity * for the number of records that may be stored. This can be determined from the value * of the {@link ElementaryFiles#MAX_RECORDS} column. *

*

* The {@link SimRecords#PHONE_NUMBER} column can only contain dialable characters and this * applies regardless of the SIM that is being used. See * {@link android.telephony.PhoneNumberUtils#isDialable(char)} for more details. Additionally * the phone number can contain at most {@link ElementaryFiles#PHONE_NUMBER_MAX_LENGTH} * characters. The {@link SimRecords#NAME} column can contain at most * {@link ElementaryFiles#NAME_MAX_LENGTH} bytes when it is encoded for storage on the SIM. * Encoding is done internally and so the name should be provided to these provider APIs as a * Java String but the number of bytes required to encode it for storage will vary depending on * the characters it contains. This length can be determined by calling * {@link SimRecords#getEncodedNameLength(ContentResolver, String)}. *

*

Operations

*
*
Insert
*

* Only {@link ElementaryFiles#EF_ADN} supports inserts. {@link SimRecords#PHONE_NUMBER} * is a required column. If the value provided for this column is missing, null, empty * or violates the requirements discussed in the Data * section above an {@link IllegalArgumentException} will be thrown. The * {@link SimRecords#NAME} column may be omitted but if provided and it violates any of * the requirements discussed in the Data section above * an {@link IllegalArgumentException} will be thrown. *

*

* If an insert is not possible because the elementary file is full then an * {@link IllegalStateException} will be thrown. *

*
Update
*

* Updates can only be performed for individual records on {@link ElementaryFiles#EF_ADN}. * A specific record is referenced via the Uri returned by * {@link SimRecords#getItemUri(int, int, int)}. Updates have the same constraints and * behavior for the {@link SimRecords#PHONE_NUMBER} and {@link SimRecords#NAME} as insert. * However, in the case of update the {@link SimRecords#PHONE_NUMBER} may be omitted as * the existing record will already have a valid value. *

*
Delete
*

* Delete may only be performed for individual records on {@link ElementaryFiles#EF_ADN}. * Deleting records will free up space for use by future inserts. *

*
Query
*

* All the records stored on a specific elementary file can be read via a Uri returned by * {@link SimRecords#getContentUri(int, int)}. This query always returns all records; there * is no support for filtering via a selection. An individual record can be queried via a Uri * returned by {@link SimRecords#getItemUri(int, int, int)}. Queries will throw an * {@link IllegalArgumentException} when the SIM with the subscription ID or the elementary file * type are invalid or unavailable. *

*
*/ public static final class SimRecords { /** * The subscription ID of the SIM the record is from. * * @see SubscriptionInfo#getSubscriptionId() */ public static final String SUBSCRIPTION_ID = "subscription_id"; /** * The type of the elementary file the record is from. * * @see ElementaryFiles#EF_ADN * @see ElementaryFiles#EF_FDN * @see ElementaryFiles#EF_SDN */ public static final String ELEMENTARY_FILE_TYPE = "elementary_file_type"; /** * The 1-based offset of the record in the elementary file that contains it. * *

This can be used to access individual SIM records by appending it to the * elementary file URIs but it is not like a normal database ID because it is not * auto-incrementing and it is not unique across SIM cards or elementary files. Hence, care * should be taken when using it to ensure that it is applied to the correct SIM and EF. * * @see #getItemUri(int, int, int) */ public static final String RECORD_NUMBER = "record_number"; /** * The name for this record. * *

An {@link IllegalArgumentException} will be thrown by insert and update if this * exceeds the maximum supported length. Use * {@link #getEncodedNameLength(ContentResolver, String)} to check how long the name * will be after encoding. * * @see ElementaryFiles#NAME_MAX_LENGTH * @see #getEncodedNameLength(ContentResolver, String) */ public static final String NAME = "name"; /** * The phone number for this record. * *

Only dialable characters are supported. * *

An {@link IllegalArgumentException} will be thrown by insert and update if this * exceeds the maximum supported length or contains unsupported characters. * * @see ElementaryFiles#PHONE_NUMBER_MAX_LENGTH * @see android.telephony.PhoneNumberUtils#isDialable(char) */ public static final String PHONE_NUMBER = "phone_number"; /** The MIME type of a CONTENT_URI subdirectory of a single SIM record. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sim-contact_v2"; /** The MIME type of CONTENT_URI providing a directory of SIM records. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/sim-contact_v2"; /** * Value returned from {@link #getEncodedNameLength(ContentResolver, String)} when the name * length could not be determined because the name could not be encoded. */ public static final int ERROR_NAME_UNSUPPORTED = -1; /** * The method name used to get the encoded length of a value for {@link SimRecords#NAME} * column. * * @hide * @see #getEncodedNameLength(ContentResolver, String) * @see ContentResolver#call(String, String, String, Bundle) */ public static final String GET_ENCODED_NAME_LENGTH_METHOD_NAME = "get_encoded_name_length"; /** * Extra key used for an integer value that contains the length in bytes of an encoded * name. * * @hide * @see #getEncodedNameLength(ContentResolver, String) * @see #GET_ENCODED_NAME_LENGTH_METHOD_NAME */ public static final String EXTRA_ENCODED_NAME_LENGTH = "android.provider.extra.ENCODED_NAME_LENGTH"; /** * Key for the PIN2 needed to modify FDN record that should be passed in the Bundle * passed to {@link ContentResolver#insert(Uri, ContentValues, Bundle)}, * {@link ContentResolver#update(Uri, ContentValues, Bundle)} * and {@link ContentResolver#delete(Uri, Bundle)}. * *

Modifying FDN records also requires either * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or * {@link TelephonyManager#hasCarrierPrivileges()} * * @hide */ @SystemApi public static final String QUERY_ARG_PIN2 = "android:query-arg-pin2"; private SimRecords() { } /** * Returns the content Uri for the specified elementary file on the specified SIM. * *

When queried this Uri will return all of the contact records in the specified * elementary file on the specified SIM. The available subscriptionIds and efTypes can * be discovered by querying {@link ElementaryFiles#CONTENT_URI}. * *

If a SIM with the provided subscription ID does not exist or the SIM with the provided * subscription ID doesn't support the specified entity file then all operations will * throw an {@link IllegalArgumentException}. * * @param subscriptionId the subscriptionId of the SIM card that this Uri will reference * @param efType the elementary file on the SIM that this Uri will reference * @see ElementaryFiles#EF_ADN * @see ElementaryFiles#EF_FDN * @see ElementaryFiles#EF_SDN */ @NonNull public static Uri getContentUri(int subscriptionId, @ElementaryFiles.EfType int efType) { return buildContentUri(subscriptionId, efType).build(); } /** * Content Uri for the specific SIM record with the provided {@link #RECORD_NUMBER}. * *

When queried this will return the record identified by the provided arguments. * *

For a non-existent record: *

* * @param subscriptionId the subscription ID of the SIM containing the record. If no SIM * with this subscription ID exists then it will be treated as a * non-existent record * @param efType the elementary file type containing the record. If the specified * SIM doesn't support this elementary file then it will be treated * as a non-existent record. * @param recordNumber the record number of the record this Uri should reference. This * must be greater than 0. If there is no record with this record * number in the specified entity file then it will be treated as a * non-existent record. * @see ElementaryFiles#SUBSCRIPTION_ID * @see ElementaryFiles#EF_TYPE * @see #RECORD_NUMBER */ @NonNull public static Uri getItemUri( int subscriptionId, @ElementaryFiles.EfType int efType, @IntRange(from = 1) int recordNumber) { // Elementary file record indices are 1-based. Preconditions.checkArgument(recordNumber > 0, "Invalid recordNumber"); return buildContentUri(subscriptionId, efType) .appendPath(String.valueOf(recordNumber)) .build(); } /** * Returns the number of bytes required to encode the specified name when it is stored * on the SIM. * *

{@link ElementaryFiles#NAME_MAX_LENGTH} is specified in bytes but the encoded name * may require more than 1 byte per character depending on the characters it contains. So * this method can be used to check whether a name exceeds the max length. * * @return the number of bytes required by the encoded name or * {@link #ERROR_NAME_UNSUPPORTED} if the name could not be encoded. * @throws IllegalStateException if the provider fails to return the length. * @see SimRecords#NAME * @see ElementaryFiles#NAME_MAX_LENGTH */ @WorkerThread @IntRange(from = 0) public static int getEncodedNameLength( @NonNull ContentResolver resolver, @NonNull String name) { Objects.requireNonNull(name); Bundle result = resolver.call(AUTHORITY, GET_ENCODED_NAME_LENGTH_METHOD_NAME, name, null); if (result == null || !result.containsKey(EXTRA_ENCODED_NAME_LENGTH)) { throw new IllegalStateException("Provider malfunction: no length was returned."); } int length = result.getInt(EXTRA_ENCODED_NAME_LENGTH, ERROR_NAME_UNSUPPORTED); if (length < 0 && length != ERROR_NAME_UNSUPPORTED) { throw new IllegalStateException( "Provider malfunction: invalid length was returned."); } return length; } private static Uri.Builder buildContentUri( int subscriptionId, @ElementaryFiles.EfType int efType) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(AUTHORITY) .appendPath(SUBSCRIPTION_ID_PATH_SEGMENT) .appendPath(String.valueOf(subscriptionId)) .appendPath(getEfUriPath(efType)); } } /** * Constants for metadata about the elementary files of the SIM cards in the phone. * *

Operations

*
*
Insert
*

Insert is not supported for the Uris defined in this class.

*
Update
*

Update is not supported for the Uris defined in this class.

*
Delete
*

Delete is not supported for the Uris defined in this class.

*
Query
*

* The elementary files for all the inserted SIMs can be read via * {@link ElementaryFiles#CONTENT_URI}. Unsupported elementary files are omitted from the * results. This Uri always returns all supported elementary files for all available SIMs; it * does not support filtering via a selection. A specific elementary file can be queried * via a Uri returned by {@link ElementaryFiles#getItemUri(int, int)}. If the elementary file * referenced by this Uri is unsupported by the SIM then the query will return an empty cursor. *

*
*/ public static final class ElementaryFiles { /** {@link SubscriptionInfo#getSimSlotIndex()} of the SIM for this row. */ public static final String SLOT_INDEX = "slot_index"; /** {@link SubscriptionInfo#getSubscriptionId()} of the SIM for this row. */ public static final String SUBSCRIPTION_ID = "subscription_id"; /** * The elementary file type for this row. * * @see ElementaryFiles#EF_ADN * @see ElementaryFiles#EF_FDN * @see ElementaryFiles#EF_SDN */ public static final String EF_TYPE = "ef_type"; /** The maximum number of records supported by the elementary file. */ public static final String MAX_RECORDS = "max_records"; /** Count of the number of records that are currently stored in the elementary file. */ public static final String RECORD_COUNT = "record_count"; /** The maximum length supported for the name of a record in the elementary file. */ public static final String NAME_MAX_LENGTH = "name_max_length"; /** * The maximum length supported for the phone number of a record in the elementary file. */ public static final String PHONE_NUMBER_MAX_LENGTH = "phone_number_max_length"; /** * A value for an elementary file that is not recognized. * *

Generally this should be ignored. If new values are added then this will be used * for apps that target SDKs where they aren't defined. */ public static final int EF_UNKNOWN = 0; /** * Type for accessing records in the "abbreviated dialing number" (ADN) elementary file on * the SIM. * *

ADN records are typically user created. */ public static final int EF_ADN = 1; /** * Type for accessing records in the "fixed dialing number" (FDN) elementary file on the * SIM. * *

FDN numbers are the numbers that are allowed to dialed for outbound calls when FDN is * enabled. * *

FDN records cannot be modified by applications. Hence, insert, update and * delete methods operating on this Uri will throw UnsupportedOperationException */ public static final int EF_FDN = 2; /** * Type for accessing records in the "service dialing number" (SDN) elementary file on the * SIM. * *

Typically SDNs are preset numbers provided by the carrier for common operations (e.g. * voicemail, check balance, etc). * *

SDN records cannot be modified by applications. Hence, insert, update and delete * methods operating on this Uri will throw UnsupportedOperationException */ public static final int EF_SDN = 3; /** * The Uri path segment used to target the ADN elementary file for SimPhonebookProvider * content operations. * * @hide */ public static final String PATH_SEGMENT_EF_ADN = "adn"; /** * The Uri path segment used to target the FDN elementary file for SimPhonebookProvider * content operations. * * @hide */ public static final String PATH_SEGMENT_EF_FDN = "fdn"; /** * The Uri path segment used to target the SDN elementary file for SimPhonebookProvider * content operations. * * @hide */ public static final String PATH_SEGMENT_EF_SDN = "sdn"; /** The MIME type of CONTENT_URI providing a directory of ADN-like elementary files. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/sim-elementary-file"; /** The MIME type of a CONTENT_URI subdirectory of a single ADN-like elementary file. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sim-elementary-file"; /** * The Uri path segment used to construct Uris for the metadata defined in this class. * * @hide */ public static final String ELEMENTARY_FILES_PATH_SEGMENT = "elementary_files"; /** Content URI for the ADN-like elementary files available on the device. */ @NonNull public static final Uri CONTENT_URI = AUTHORITY_URI .buildUpon() .appendPath(ELEMENTARY_FILES_PATH_SEGMENT).build(); private ElementaryFiles() { } /** * Returns a content uri for a specific elementary file. * *

If a SIM with the specified subscriptionId is not present an exception will be thrown. * If the SIM doesn't support the specified elementary file it will return an empty cursor. */ @NonNull public static Uri getItemUri(int subscriptionId, @EfType int efType) { return CONTENT_URI.buildUpon().appendPath(SUBSCRIPTION_ID_PATH_SEGMENT) .appendPath(String.valueOf(subscriptionId)) .appendPath(getEfUriPath(efType)) .build(); } /** * Annotation for the valid elementary file types. * * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"EF"}, value = {EF_UNKNOWN, EF_ADN, EF_FDN, EF_SDN}) public @interface EfType { } } }