Merge "Add UCS-2 support for SimPhonebookContract"

This commit is contained in:
Treehugger Robot
2021-02-17 08:07:39 +00:00
committed by Gerrit Code Review
5 changed files with 90 additions and 231 deletions

View File

@@ -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();
}

View File

@@ -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 {

View File

@@ -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.
*
* @hide
* @see #validateName(ContentResolver, int, int, String)
* Value returned from {@link #getEncodedNameLength(ContentResolver, String)} when the name
* length could not be determined because the name could not be encoded.
*/
@SystemApi
public static final String VALIDATE_NAME_PATH_SEGMENT = "validate_name";
public static final int ERROR_NAME_UNSUPPORTED = -1;
/**
* The key for a cursor extra that contains the result of a validate name query.
* 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 EXTRA_NAME_VALIDATION_RESULT =
"android.provider.extra.NAME_VALIDATION_RESULT";
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";
/**
@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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;