Merge "Add UCS-2 support for SimPhonebookContract"
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user