diff --git a/core/api/current.txt b/core/api/current.txt index f6164af5588956acb29433ef6e9d2282044ca830..23f490aa6275e2270fa3c15647f2fbb5a92f23c7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34350,30 +34350,18 @@ package android.provider { public static final class SimPhonebookContract.SimRecords { method @NonNull public static android.net.Uri getContentUri(int, int); + method @WorkerThread public static int getEncodedNameLength(@NonNull android.content.ContentResolver, @NonNull String); method @NonNull public static android.net.Uri getItemUri(int, int, int); - method @NonNull @WorkerThread public static android.provider.SimPhonebookContract.SimRecords.NameValidationResult validateName(@NonNull android.content.ContentResolver, int, int, @NonNull String); field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/sim-contact_v2"; field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/sim-contact_v2"; field public static final String ELEMENTARY_FILE_TYPE = "elementary_file_type"; + field public static final int ERROR_NAME_UNSUPPORTED = -1; // 0xffffffff field public static final String NAME = "name"; field public static final String PHONE_NUMBER = "phone_number"; field public static final String RECORD_NUMBER = "record_number"; field public static final String SUBSCRIPTION_ID = "subscription_id"; } - public static final class SimPhonebookContract.SimRecords.NameValidationResult implements android.os.Parcelable { - ctor public SimPhonebookContract.SimRecords.NameValidationResult(@NonNull String, @NonNull String, int, int); - method public int describeContents(); - method public int getEncodedLength(); - method public int getMaxEncodedLength(); - method @NonNull public String getName(); - method @NonNull public String getSanitizedName(); - method public boolean isSupportedCharacter(int); - method public boolean isValid(); - method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.provider.SimPhonebookContract.SimRecords.NameValidationResult> CREATOR; - } - public class SyncStateContract { ctor public SyncStateContract(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 84d04fceaaba2719f7c2f89d656424ceb0068a8b..ca3b24ad3cf092f983a4d878819645c18c47aa95 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -8328,22 +8328,8 @@ package android.provider { method @RequiresPermission(android.Manifest.permission.MODIFY_SETTINGS_OVERRIDEABLE_BY_RESTORE) public static boolean putString(@NonNull android.content.ContentResolver, @NonNull String, @Nullable String, boolean); } - public final class SimPhonebookContract { - method @NonNull public static String getEfUriPath(int); - field public static final String SUBSCRIPTION_ID_PATH_SEGMENT = "subid"; - } - - public static final class SimPhonebookContract.ElementaryFiles { - field public static final String EF_ADN_PATH_SEGMENT = "adn"; - field public static final String EF_FDN_PATH_SEGMENT = "fdn"; - field public static final String EF_SDN_PATH_SEGMENT = "sdn"; - field public static final String ELEMENTARY_FILES_PATH_SEGMENT = "elementary_files"; - } - public static final class SimPhonebookContract.SimRecords { - field public static final String EXTRA_NAME_VALIDATION_RESULT = "android.provider.extra.NAME_VALIDATION_RESULT"; field public static final String QUERY_ARG_PIN2 = "android:query-arg-pin2"; - field public static final String VALIDATE_NAME_PATH_SEGMENT = "validate_name"; } public static final class Telephony.Carriers implements android.provider.BaseColumns { diff --git a/core/java/android/provider/SimPhonebookContract.java b/core/java/android/provider/SimPhonebookContract.java index 2efc21229422a4a94c804c63d9940933285ec089..f3a78562f73a8b8153f3d276a3670564fa81d8f4 100644 --- a/core/java/android/provider/SimPhonebookContract.java +++ b/core/java/android/provider/SimPhonebookContract.java @@ -29,11 +29,8 @@ import android.annotation.SystemApi; import android.annotation.WorkerThread; import android.content.ContentResolver; import android.content.ContentValues; -import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; import android.telephony.SubscriptionInfo; import android.telephony.TelephonyManager; @@ -63,7 +60,6 @@ public final class SimPhonebookContract { * * @hide */ - @SystemApi public static final String SUBSCRIPTION_ID_PATH_SEGMENT = "subid"; private SimPhonebookContract() { @@ -76,7 +72,6 @@ public final class SimPhonebookContract { * @hide */ @NonNull - @SystemApi public static String getEfUriPath(@ElementaryFiles.EfType int efType) { switch (efType) { case EF_ADN: @@ -122,12 +117,12 @@ public final class SimPhonebookContract { * The name for this record. * * <p>An {@link IllegalArgumentException} will be thrown by insert and update if this - * exceeds the maximum supported length or contains unsupported characters. - * {@link #validateName(ContentResolver, int, int, String)} )} can be used to - * check whether the name is supported. + * 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 #validateName(ContentResolver, int, int, String) ) + * @see #getEncodedNameLength(ContentResolver, String) */ public static final String NAME = "name"; /** @@ -149,24 +144,31 @@ public final class SimPhonebookContract { public static final String CONTENT_TYPE = "vnd.android.cursor.dir/sim-contact_v2"; /** - * The path segment that is appended to {@link #getContentUri(int, int)} which indicates - * that the following path segment contains a name to be validated. + * 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 #validateName(ContentResolver, int, int, String) + * @see #getEncodedNameLength(ContentResolver, String) + * @see ContentResolver#call(String, String, String, Bundle) */ - @SystemApi - public static final String VALIDATE_NAME_PATH_SEGMENT = "validate_name"; + public static final String GET_ENCODED_NAME_LENGTH_METHOD_NAME = "get_encoded_name_length"; /** - * The key for a cursor extra that contains the result of a validate name query. + * Extra key used for an integer value that contains the length in bytes of an encoded + * name. * * @hide - * @see #validateName(ContentResolver, int, int, String) + * @see #getEncodedNameLength(ContentResolver, String) + * @see #GET_ENCODED_NAME_LENGTH_METHOD_NAME */ - @SystemApi - public static final String EXTRA_NAME_VALIDATION_RESULT = - "android.provider.extra.NAME_VALIDATION_RESULT"; + public static final String EXTRA_ENCODED_NAME_LENGTH = + "android.provider.extra.ENCODED_NAME_LENGTH"; /** @@ -244,32 +246,34 @@ public final class SimPhonebookContract { } /** - * Validates a value that is being provided for the {@link #NAME} column. + * Returns the number of bytes required to encode the specified name when it is stored + * on the SIM. * - * <p>The return value can be used to check if the name is valid. If it is not valid then - * inserts and updates to the specified elementary file that use the provided name value - * will throw an {@link IllegalArgumentException}. + * <p>{@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. * - * <p>If the specified SIM or elementary file don't exist then - * {@link NameValidationResult#getMaxEncodedLength()} will be zero and - * {@link NameValidationResult#isValid()} will return false. + * @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 */ - @NonNull @WorkerThread - public static NameValidationResult validateName( - @NonNull ContentResolver resolver, int subscriptionId, - @ElementaryFiles.EfType int efType, - @NonNull String name) { - Bundle queryArgs = new Bundle(); - queryArgs.putString(SimRecords.NAME, name); - try (Cursor cursor = - resolver.query(buildContentUri(subscriptionId, efType) - .appendPath(VALIDATE_NAME_PATH_SEGMENT) - .build(), null, queryArgs, null)) { - NameValidationResult result = cursor.getExtras() - .getParcelable(EXTRA_NAME_VALIDATION_RESULT); - return result != null ? result : new NameValidationResult(name, "", 0, 0); + public static int getEncodedNameLength( + @NonNull ContentResolver resolver, @NonNull String name) { + 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( @@ -281,106 +285,6 @@ public final class SimPhonebookContract { .appendPath(getEfUriPath(efType)); } - /** Contains details about the validity of a value provided for the {@link #NAME} column. */ - public static final class NameValidationResult implements Parcelable { - - @NonNull - public static final Creator<NameValidationResult> CREATOR = - new Creator<NameValidationResult>() { - - @Override - public NameValidationResult createFromParcel(@NonNull Parcel in) { - return new NameValidationResult(in); - } - - @NonNull - @Override - public NameValidationResult[] newArray(int size) { - return new NameValidationResult[size]; - } - }; - - private final String mName; - private final String mSanitizedName; - private final int mEncodedLength; - private final int mMaxEncodedLength; - - /** Creates a new instance from the provided values. */ - public NameValidationResult(@NonNull String name, @NonNull String sanitizedName, - int encodedLength, int maxEncodedLength) { - this.mName = Objects.requireNonNull(name); - this.mSanitizedName = Objects.requireNonNull(sanitizedName); - this.mEncodedLength = encodedLength; - this.mMaxEncodedLength = maxEncodedLength; - } - - private NameValidationResult(Parcel in) { - this(in.readString(), in.readString(), in.readInt(), in.readInt()); - } - - /** Returns the original name that is being validated. */ - @NonNull - public String getName() { - return mName; - } - - /** - * Returns a sanitized copy of the original name with all unsupported characters - * replaced with spaces. - */ - @NonNull - public String getSanitizedName() { - return mSanitizedName; - } - - /** - * Returns whether the original name isValid. - * - * <p>If this returns false then inserts and updates using the name will throw an - * {@link IllegalArgumentException} - */ - public boolean isValid() { - return mMaxEncodedLength > 0 && mEncodedLength <= mMaxEncodedLength - && Objects.equals( - mName, mSanitizedName); - } - - /** Returns whether the character at the specified position is supported by the SIM. */ - public boolean isSupportedCharacter(int position) { - return mName.charAt(position) == mSanitizedName.charAt(position); - } - - /** - * Returns the number of bytes required to save the name. - * - * <p>This may be more than the number of characters in the name. - */ - public int getEncodedLength() { - return mEncodedLength; - } - - /** - * Returns the maximum number of bytes that are supported for the name. - * - * @see ElementaryFiles#NAME_MAX_LENGTH - */ - public int getMaxEncodedLength() { - return mMaxEncodedLength; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString(mName); - dest.writeString(mSanitizedName); - dest.writeInt(mEncodedLength); - dest.writeInt(mMaxEncodedLength); - } - } } /** Constants for metadata about the elementary files of the SIM cards in the phone. */ @@ -446,13 +350,10 @@ public final class SimPhonebookContract { */ public static final int EF_SDN = 3; /** @hide */ - @SystemApi public static final String EF_ADN_PATH_SEGMENT = "adn"; /** @hide */ - @SystemApi public static final String EF_FDN_PATH_SEGMENT = "fdn"; /** @hide */ - @SystemApi public static final String EF_SDN_PATH_SEGMENT = "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"; @@ -464,7 +365,6 @@ public final class SimPhonebookContract { * * @hide */ - @SystemApi public static final String ELEMENTARY_FILES_PATH_SEGMENT = "elementary_files"; /** Content URI for the ADN-like elementary files available on the device. */ @@ -480,8 +380,7 @@ public final class SimPhonebookContract { * Returns a content uri for a specific elementary file. * * <p>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 have a zero value for - * {@link #MAX_RECORDS}. + * 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) { diff --git a/core/tests/coretests/src/android/provider/SimPhonebookContractTest.java b/core/tests/coretests/src/android/provider/SimPhonebookContractTest.java index be3826007aa357ddbe3d76e741dd82a883339236..bc7be1b0862678d545ca4f0aca7d9d5b5634d403 100644 --- a/core/tests/coretests/src/android/provider/SimPhonebookContractTest.java +++ b/core/tests/coretests/src/android/provider/SimPhonebookContractTest.java @@ -16,14 +16,8 @@ package android.provider; -import static com.google.common.truth.Truth.assertThat; - import static org.testng.Assert.assertThrows; -import android.content.ContentValues; -import android.os.Parcel; -import android.provider.SimPhonebookContract.SimRecords.NameValidationResult; - import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -71,50 +65,5 @@ public class SimPhonebookContractTest { SimPhonebookContract.ElementaryFiles.EF_ADN, -1) ); } - - @Test - public void nameValidationResult_isValid_validNames() { - assertThat(new NameValidationResult("", "", 0, 1).isValid()).isTrue(); - assertThat(new NameValidationResult("a", "a", 1, 1).isValid()).isTrue(); - assertThat(new NameValidationResult("First Last", "First Last", 10, 10).isValid()).isTrue(); - assertThat( - new NameValidationResult("First Last", "First Last", 10, 100).isValid()).isTrue(); - } - - @Test - public void nameValidationResult_isValid_invalidNames() { - assertThat(new NameValidationResult("", "", 0, 0).isValid()).isFalse(); - assertThat(new NameValidationResult("ab", "ab", 2, 1).isValid()).isFalse(); - NameValidationResult unsupportedCharactersResult = new NameValidationResult("A_b_c", - "A b c", 5, 5); - assertThat(unsupportedCharactersResult.isValid()).isFalse(); - assertThat(unsupportedCharactersResult.isSupportedCharacter(0)).isTrue(); - assertThat(unsupportedCharactersResult.isSupportedCharacter(1)).isFalse(); - assertThat(unsupportedCharactersResult.isSupportedCharacter(2)).isTrue(); - assertThat(unsupportedCharactersResult.isSupportedCharacter(3)).isFalse(); - assertThat(unsupportedCharactersResult.isSupportedCharacter(4)).isTrue(); - } - - @Test - public void nameValidationResult_parcel() { - ContentValues values = new ContentValues(); - values.put("name", "Name"); - values.put("phone_number", "123"); - - NameValidationResult result; - Parcel parcel = Parcel.obtain(); - try { - parcel.writeParcelable(new NameValidationResult("name", "sanitized name", 1, 2), 0); - parcel.setDataPosition(0); - result = parcel.readParcelable(NameValidationResult.class.getClassLoader()); - } finally { - parcel.recycle(); - } - - assertThat(result.getName()).isEqualTo("name"); - assertThat(result.getSanitizedName()).isEqualTo("sanitized name"); - assertThat(result.getEncodedLength()).isEqualTo(1); - assertThat(result.getMaxEncodedLength()).isEqualTo(2); - } } diff --git a/telephony/java/com/android/internal/telephony/uicc/IccUtils.java b/telephony/java/com/android/internal/telephony/uicc/IccUtils.java index d79225fe536960412de32c7c9e6205bd706b050d..ec12040422604a4884409036b7ad05b7e6cab276 100644 --- a/telephony/java/com/android/internal/telephony/uicc/IccUtils.java +++ b/telephony/java/com/android/internal/telephony/uicc/IccUtils.java @@ -16,6 +16,7 @@ package com.android.internal.telephony.uicc; +import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; @@ -28,6 +29,7 @@ import com.android.internal.telephony.GsmAlphabet; import com.android.telephony.Rlog; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.List; /** @@ -253,12 +255,47 @@ public class IccUtils { } if ((b & 0x0f) <= 0x09) { - ret += (b & 0xf); + ret += (b & 0xf); } return ret; } + /** + * Encodes a string to be formatted like the EF[ADN] alpha identifier. + * + * <p>See javadoc for {@link #adnStringFieldToString(byte[], int, int)} for more details on + * the relevant specs. + * + * <p>This will attempt to encode using the GSM 7-bit alphabet but will fallback to UCS-2 if + * there are characters that are not supported by it. + * + * @return the encoded string including the prefix byte necessary to identify the encoding. + * @see #adnStringFieldToString(byte[], int, int) + */ + @NonNull + public static byte[] stringToAdnStringField(@NonNull String alphaTag) { + int septets = GsmAlphabet.countGsmSeptetsUsingTables(alphaTag, false, 0, 0); + if (septets != -1) { + byte[] ret = new byte[septets]; + GsmAlphabet.stringToGsm8BitUnpackedField(alphaTag, ret, 0, ret.length); + return ret; + } + + // Strictly speaking UCS-2 disallows surrogate characters but it's much more complicated to + // validate that the string contains only valid UCS-2 characters. Since the read path + // in most modern software will decode "UCS-2" by treating it as UTF-16 this should be fine + // (e.g. the adnStringFieldToString has done this for a long time on Android). Also there's + // already a precedent in SMS applications to ignore the UCS-2/UTF-16 distinction. + byte[] alphaTagBytes = alphaTag.getBytes(StandardCharsets.UTF_16BE); + byte[] ret = new byte[alphaTagBytes.length + 1]; + // 0x80 tags the remaining bytes as UCS-2 + ret[0] = (byte) 0x80; + System.arraycopy(alphaTagBytes, 0, ret, 1, alphaTagBytes.length); + + return ret; + } + /** * Decodes a string field that's formatted like the EF[ADN] alpha * identifier @@ -309,7 +346,7 @@ public class IccUtils { ret = new String(data, offset + 1, ucslen * 2, "utf-16be"); } catch (UnsupportedEncodingException ex) { Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException", - ex); + ex); } if (ret != null) { @@ -342,7 +379,7 @@ public class IccUtils { len = length - 4; base = (char) (((data[offset + 2] & 0xFF) << 8) | - (data[offset + 3] & 0xFF)); + (data[offset + 3] & 0xFF)); offset += 4; isucs2 = true; } @@ -366,7 +403,7 @@ public class IccUtils { count++; ret.append(GsmAlphabet.gsm8BitUnpackedToString(data, - offset, count)); + offset, count)); offset += count; len -= count;