diff options
-rw-r--r-- | core/TEST_MAPPING | 12 | ||||
-rw-r--r-- | core/api/current.txt | 49 | ||||
-rw-r--r-- | core/java/android/app/SystemServiceRegistry.java | 13 | ||||
-rw-r--r-- | core/java/android/content/Context.java | 11 | ||||
-rw-r--r-- | core/java/android/provider/ContactKeysManager.java | 1129 | ||||
-rw-r--r-- | core/java/android/provider/flags.aconfig | 7 |
6 files changed, 1220 insertions, 1 deletions
diff --git a/core/TEST_MAPPING b/core/TEST_MAPPING index fd571c95f568..24ba5c4e1281 100644 --- a/core/TEST_MAPPING +++ b/core/TEST_MAPPING @@ -20,5 +20,15 @@ "core/tests/coretests/src/com/android/internal/inputmethod/.*" ] } - ] + ], + "postsubmit": [ + { + "name": "ContactKeysManagerTest", + "options": [ + { + "include-filter": "android.provider.cts.contactkeys." + } + ] + } + ] } diff --git a/core/api/current.txt b/core/api/current.txt index 66feebc6cc92..89c772826822 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10604,6 +10604,7 @@ package android.content { field public static final String CONNECTIVITY_DIAGNOSTICS_SERVICE = "connectivity_diagnostics"; field public static final String CONNECTIVITY_SERVICE = "connectivity"; field public static final String CONSUMER_IR_SERVICE = "consumer_ir"; + field @FlaggedApi("android.provider.user_keys") public static final String CONTACT_KEYS_SERVICE = "contact_keys"; field public static final int CONTEXT_IGNORE_SECURITY = 2; // 0x2 field public static final int CONTEXT_INCLUDE_CODE = 1; // 0x1 field public static final int CONTEXT_RESTRICTED = 4; // 0x4 @@ -35231,6 +35232,54 @@ package android.provider { field public static final String LONGITUDE = "longitude"; } + @FlaggedApi("android.provider.user_keys") public class ContactKeysManager { + method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.ContactKey> getAllContactKeys(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.SelfKey> getAllSelfKeys(); + method @Nullable @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public android.provider.ContactKeysManager.ContactKey getContactKey(@NonNull String, @NonNull String, @NonNull String); + method public static int getMaxKeySizeBytes(); + method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.ContactKey> getOwnerContactKeys(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.SelfKey> getOwnerSelfKeys(); + method @Nullable @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public android.provider.ContactKeysManager.SelfKey getSelfKey(@NonNull String, @NonNull String); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean removeContactKey(@NonNull String, @NonNull String, @NonNull String); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean removeSelfKey(@NonNull String, @NonNull String); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, int); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public void updateOrInsertContactKey(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateOrInsertSelfKey(@NonNull String, @NonNull String, @NonNull byte[]); + method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, int); + field public static final int UNVERIFIED = 0; // 0x0 + field public static final int VERIFICATION_FAILED = 1; // 0x1 + field public static final int VERIFIED = 2; // 0x2 + } + + public static final class ContactKeysManager.ContactKey implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getAccountId(); + method @Nullable public String getDeviceId(); + method @Nullable public String getDisplayName(); + method @Nullable public String getEmailAddress(); + method @Nullable public byte[] getKeyValue(); + method public int getLocalVerificationState(); + method @NonNull public String getOwnerPackageName(); + method @Nullable public String getPhoneNumber(); + method public int getRemoteVerificationState(); + method public long getTimeUpdated(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.provider.ContactKeysManager.ContactKey> CREATOR; + } + + public static final class ContactKeysManager.SelfKey implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getAccountId(); + method @Nullable public String getDeviceId(); + method @Nullable public byte[] getKeyValue(); + method @NonNull public String getOwnerPackageName(); + method public int getRemoteVerificationState(); + method public long getTimeUpdated(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.provider.ContactKeysManager.SelfKey> CREATOR; + } + @Deprecated public class Contacts { field @Deprecated public static final String AUTHORITY = "contacts"; field @Deprecated public static final android.net.Uri CONTENT_URI; diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index d7554137fa5b..397b63fe6a30 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -210,6 +210,7 @@ import android.permission.PermissionControllerManager; import android.permission.PermissionManager; import android.print.IPrintManager; import android.print.PrintManager; +import android.provider.ContactKeysManager; import android.safetycenter.SafetyCenterFrameworkInitializer; import android.scheduling.SchedulingFrameworkInitializer; import android.security.FileIntegrityManager; @@ -1604,6 +1605,18 @@ public final class SystemServiceRegistry { } }); + registerService(Context.CONTACT_KEYS_SERVICE, ContactKeysManager.class, + new CachedServiceFetcher<ContactKeysManager>() { + @Override + public ContactKeysManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + if (!android.provider.Flags.userKeys()) { + throw new ServiceNotFoundException( + "ContactKeysManager is not supported"); + } + return new ContactKeysManager(ctx); + }}); + sInitializing = true; try { // Note: the following functions need to be @SystemApis, once they become mainline diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 67a3627a399f..b1173a25e95f 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4243,6 +4243,7 @@ public abstract class Context { GRAMMATICAL_INFLECTION_SERVICE, SECURITY_STATE_SERVICE, //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE, + CONTACT_KEYS_SERVICE, }) @Retention(RetentionPolicy.SOURCE) @@ -6542,6 +6543,16 @@ public abstract class Context { public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation"; /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.provider.ContactKeysManager} to managing contact keys. + * + * @see #getSystemService(String) + * @see android.provider.ContactKeysManager + */ + @FlaggedApi(android.provider.Flags.FLAG_USER_KEYS) + public static final String CONTACT_KEYS_SERVICE = "contact_keys"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java new file mode 100644 index 000000000000..ecde69917820 --- /dev/null +++ b/core/java/android/provider/ContactKeysManager.java @@ -0,0 +1,1129 @@ +/* + * Copyright (C) 2024 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 android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * ContactKeysManager provides the access to the E2EE contact keys provider. + * It manages two types of keys - {@link ContactKey} of other users' and the owner's keys - + * {@link SelfKey}. + * <ul> + * <li> + * For {@link ContactKey} this API allows the insert/update, removal, changing of the + * verification state, retrieving the keys (either created by or visible to the caller app) + * operations. + * </li> + * <li> + * For {@link SelfKey} this API allows the insert/update, removal, retrieving the self keys + * (either created by or visible to the caller app) operations. + * </li> + * </ul> + * Keys are uniquely identified by: + * <ul> + * <li> + * ownerPackageName - package name of an app that the key belongs to. + * </li> + * <li> + * deviceId - an app-specified identifier for the device, defined by the app. Within that app, + * the deviceID should be unique among devices belonging to that user. + * </li> + * <li> + * accountId - the app-specified identifier for the account for which the contact key can be used. + * Usually a phone number. + * </li> + * </ul> + * Contact keys also use lookupKey which is an opaque value used to identify a contact in + * ContactsProvider. + */ +@FlaggedApi(Flags.FLAG_USER_KEYS) +public class ContactKeysManager { + /** + * The authority for the contact keys provider. + * @hide + */ + public static final String AUTHORITY = "com.android.contactkeys.contactkeysprovider"; + + /** + * A content:// style uri to the authority for the contact keys provider. + * @hide + */ + @NonNull + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + /** + * Maximum size of a contact key. + */ + private static final int MAX_KEY_SIZE_BYTES = 5000; + + /** + * Special value to distinguish a null array in a parcelable object. + */ + private static final int ARRAY_IS_NULL = -1; + + @NonNull + private final ContentResolver mContentResolver; + + /** @hide */ + public ContactKeysManager(@NonNull Context context) { + Objects.requireNonNull(context); + mContentResolver = context.getContentResolver(); + } + + /** + * Inserts a new entry into the contact keys table or updates one if it already exists. + * The inserted/updated contact key is owned by the caller app. + * + * @param lookupKey value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param keyValue the raw bytes for the key (max size is {@link #getMaxKeySizeBytes} bytes) + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public void updateOrInsertContactKey(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @NonNull byte[] keyValue) { + validateKeyLength(keyValue); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putByteArray(ContactKeys.KEY_VALUE, Objects.requireNonNull(keyValue)); + + nullSafeCall(mContentResolver, + ContactKeys.UPDATE_OR_INSERT_CONTACT_KEY_METHOD, extras); + } + + /** + * Retrieves a contact key entry given the lookup key, device ID, accountId and inferred + * caller package name. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * + * @return a {@link ContactKey} object containing the contact key information, + * or null if no contact key is found. + */ + @RequiresPermission(android.Manifest.permission.READ_CONTACTS) + @Nullable + public ContactKey getContactKey( + @NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId) { + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.GET_CONTACT_KEY_METHOD, extras); + + if (response == null) { + return null; + } + return response.getParcelable(ContactKeys.KEY_CONTACT_KEY, ContactKey.class); + } + + /** + * Retrieves all contact key entries that belong to apps visible to the caller. + * The keys will be stripped of deviceId, timeUpdated and keyValue data. + * + * @param lookupKey the value that references the contact + * + * @return a list of {@link ContactKey} objects containing the contact key + * information, or an empty list if no keys are found. + */ + @RequiresPermission(android.Manifest.permission.READ_CONTACTS) + @NonNull + public List<ContactKey> getAllContactKeys(@NonNull String lookupKey) { + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.GET_ALL_CONTACT_KEYS_METHOD, extras); + + if (response == null) { + return new ArrayList<>(); + } + List<ContactKey> value = response.getParcelableArrayList(ContactKeys.KEY_CONTACT_KEYS, + ContactKey.class); + if (value == null) { + return new ArrayList<>(); + } + return value; + } + + /** + * Retrieves all contact key entries for a given lookupKey that belong to the caller app. + * + * @param lookupKey the value that references the contact + * + * @return a list of {@link ContactKey} objects containing the contact key + * information, or an empty list if no keys are found. + */ + @RequiresPermission(android.Manifest.permission.READ_CONTACTS) + @NonNull + public List<ContactKey> getOwnerContactKeys(@NonNull String lookupKey) { + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.GET_OWNER_CONTACT_KEYS_METHOD, extras); + + if (response == null) { + return new ArrayList<>(); + } + List<ContactKey> value = response.getParcelableArrayList(ContactKeys.KEY_CONTACT_KEYS, + ContactKey.class); + if (value == null) { + return new ArrayList<>(); + } + return value; + } + + /** + * Updates a contact key entry's local verification state that belongs to the caller app. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param localVerificationState the new local verification state + * + * @return true if the entry was updated, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public boolean updateContactKeyLocalVerificationState(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @VerificationState int localVerificationState) { + validateVerificationState(localVerificationState); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putInt(ContactKeys.LOCAL_VERIFICATION_STATE, localVerificationState); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_CONTACT_KEY_LOCAL_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** + * Updates a contact key entry's remote verification state that belongs to the caller app. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param remoteVerificationState the new remote verification state + * + * @return true if the entry was updated, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public boolean updateContactKeyRemoteVerificationState(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId, + @VerificationState int remoteVerificationState) { + validateVerificationState(remoteVerificationState); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_CONTACT_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + private static void validateVerificationState(int verificationState) { + if (verificationState != UNVERIFIED + && verificationState != VERIFICATION_FAILED + && verificationState != VERIFIED) { + throw new IllegalArgumentException("Verification state value " + + verificationState + " is not supported"); + } + } + + /** + * Removes a contact key entry that belongs to the caller app. + * + * @param lookupKey the value that references the contact + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * + * @return true if the entry was removed, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public boolean removeContactKey(@NonNull String lookupKey, + @NonNull String deviceId, + @NonNull String accountId) { + Bundle extras = new Bundle(); + extras.putString(ContactKeys.LOOKUP_KEY, Objects.requireNonNull(lookupKey)); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.REMOVE_CONTACT_KEY_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** + * Inserts a new entry into the self keys table or updates one if it already exists. + + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param keyValue the raw bytes for the key (max size is {@link #getMaxKeySizeBytes} bytes) + * + * @return true if the entry was added or updated, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public boolean updateOrInsertSelfKey(@NonNull String deviceId, + @NonNull String accountId, + @NonNull byte[] keyValue) { + validateKeyLength(keyValue); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putByteArray(ContactKeys.KEY_VALUE, Objects.requireNonNull(keyValue)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_OR_INSERT_SELF_KEY_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + private static void validateKeyLength(byte[] keyValue) { + Objects.requireNonNull(keyValue); + if (keyValue.length == 0 || keyValue.length > getMaxKeySizeBytes()) { + throw new IllegalArgumentException("Key value length is " + keyValue.length + "." + + " Should be more than 0 and less than " + getMaxKeySizeBytes()); + } + } + + /** + * Updates a self key entry's remote verification state. + * + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * @param remoteVerificationState the new remote verification state + * + * @return true if the entry was updated, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public boolean updateSelfKeyRemoteVerificationState(@NonNull String deviceId, + @NonNull String accountId, + @VerificationState int remoteVerificationState) { + validateVerificationState(remoteVerificationState); + + Bundle extras = new Bundle(); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + extras.putInt(ContactKeys.REMOTE_VERIFICATION_STATE, remoteVerificationState); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.UPDATE_SELF_KEY_REMOTE_VERIFICATION_STATE_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + /** + * Maximum size of a contact key. + */ + public static int getMaxKeySizeBytes() { + return MAX_KEY_SIZE_BYTES; + } + + /** + * Returns a self key entry given the deviceId and the inferred package name of the caller. + * + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * + * @return a {@link SelfKey} object containing the self key information, or null if no self key + * is found. + */ + @RequiresPermission(android.Manifest.permission.READ_CONTACTS) + @Nullable + public SelfKey getSelfKey(@NonNull String deviceId, + @NonNull String accountId) { + Bundle extras = new Bundle(); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.GET_SELF_KEY_METHOD, extras); + + if (response == null) { + return null; + } + return response.getParcelable(ContactKeys.KEY_CONTACT_KEY, SelfKey.class); + } + + /** + * Returns all self key entries that belong to apps visible to the caller. + * The keys will be stripped of deviceId, timeUpdated and keyValue data. + * + * @return a list of {@link SelfKey} objects containing the self key information, or + * an empty list if no keys are found. + */ + @RequiresPermission(android.Manifest.permission.READ_CONTACTS) + @NonNull + public List<SelfKey> getAllSelfKeys() { + Bundle extras = new Bundle(); + + Bundle response = nullSafeCall(mContentResolver, ContactKeys.GET_ALL_SELF_KEYS_METHOD, + extras); + + if (response == null) { + return new ArrayList<>(); + } + List<SelfKey> value = response.getParcelableArrayList(ContactKeys.KEY_CONTACT_KEYS, + SelfKey.class); + if (value == null) { + return new ArrayList<>(); + } + return value; + } + + /** + * Returns all self key entries that are owned by the caller app. + * + * @return a list of {@link SelfKey} objects containing the self key information, or + * an empty list if no keys are found. + */ + @RequiresPermission(android.Manifest.permission.READ_CONTACTS) + @NonNull + public List<SelfKey> getOwnerSelfKeys() { + Bundle extras = new Bundle(); + + Bundle response = nullSafeCall(mContentResolver, ContactKeys.GET_OWNER_SELF_KEYS_METHOD, + extras); + + if (response == null) { + return new ArrayList<>(); + } + List<SelfKey> value = response.getParcelableArrayList(ContactKeys.KEY_CONTACT_KEYS, + SelfKey.class); + if (value == null) { + return new ArrayList<>(); + } + return value; + } + + /** + * Removes a self key entry given the deviceId and the inferred package name of the caller. + + * @param deviceId an app-specified identifier for the device + * @param accountId an app-specified identifier for the account + * + * @return true if the entry was removed, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) + public boolean removeSelfKey(@NonNull String deviceId, + @NonNull String accountId) { + Bundle extras = new Bundle(); + extras.putString(ContactKeys.DEVICE_ID, Objects.requireNonNull(deviceId)); + extras.putString(ContactKeys.ACCOUNT_ID, Objects.requireNonNull(accountId)); + + Bundle response = nullSafeCall(mContentResolver, + ContactKeys.REMOVE_SELF_KEY_METHOD, extras); + + return response != null && response.getBoolean(ContactKeys.KEY_UPDATED_ROWS); + } + + private Bundle nullSafeCall(@NonNull ContentResolver resolver, @NonNull String method, + @Nullable Bundle extras) { + try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY_URI)) { + return client.call(method, null, extras); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + /** + * Possible values of verification state. + * @hide + */ + @IntDef(prefix = {"VERIFICATION_STATE_"}, value = { + UNVERIFIED, + VERIFICATION_FAILED, + VERIFIED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VerificationState {} + + /** + * Unverified state of a contact E2EE key. + */ + public static final int UNVERIFIED = 0; + /** + * Failed verification state of a contact E2EE key. + */ + public static final int VERIFICATION_FAILED = 1; + /** + * Verified state of a contact E2EE key. + */ + public static final int VERIFIED = 2; + + /** @hide */ + public static final class ContactKeys { + + private ContactKeys() {} + + /** + * <p> + * An opaque value that contains hints on how to find the contact if + * its row id changed as a result of a sync or aggregation. + * </p> + */ + public static final String LOOKUP_KEY = "lookup"; + + /** + * <p> + * An app-specified identifier for the device for which the contact key can be used. + * </p> + */ + public static final String DEVICE_ID = "device_id"; + + /** + * <p> + * An app-specified identifier for the account for which the contact key can be used. + * Usually a phone number. + * </p> + */ + public static final String ACCOUNT_ID = "account_id"; + + /** + * <p> + * The display name for the contact. + * </p> + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * <p> + * The phone number as the user entered it. + * </p> + */ + public static final String PHONE_NUMBER = "number"; + + /** + * <p> + * The email address. + * </p> + */ + public static final String EMAIL_ADDRESS = "address"; + + /** + * <p> + * Timestamp at which the key was updated. + * </p> + */ + public static final String TIME_UPDATED = "time_updated"; + + /** + * <p> + * The raw bytes for the key. + * </p> + */ + public static final String KEY_VALUE = "key_value"; + + /** + * <p> + * The package name of the package that created the key. + * </p> + */ + public static final String OWNER_PACKAGE_NAME = "owner_package_name"; + + /** + * <p> + * Describes the local verification state for the key, for instance QR-code based + * verification. + * </p> + */ + public static final String LOCAL_VERIFICATION_STATE = "local_verification_state"; + + /** + * <p> + * Describes the remote verification state for the key, for instance through a key + * transparency server. + * </p> + */ + public static final String REMOTE_VERIFICATION_STATE = "remote_verification_state"; + + /** + * The method to invoke in order to add a new key for a contact. + */ + public static final String UPDATE_OR_INSERT_CONTACT_KEY_METHOD = "updateOrInsertContactKey"; + + /** + * The method to invoke in order to retrieve key for a single contact. + */ + public static final String GET_CONTACT_KEY_METHOD = "getContactKey"; + + /** + * The method to invoke in order to retrieve all contact keys. + */ + public static final String GET_ALL_CONTACT_KEYS_METHOD = "getAllContactKeys"; + + /** + * The method to invoke in order to retrieve contact keys that belong to the caller. + */ + public static final String GET_OWNER_CONTACT_KEYS_METHOD = "getOwnerContactKeys"; + + /** + * The method to invoke in order to update a contact key local verification state. + */ + public static final String UPDATE_CONTACT_KEY_LOCAL_VERIFICATION_STATE_METHOD = + "updateContactKeyLocalVerificationState"; + + /** + * The method to invoke in order to update a contact key remote verification state. + */ + public static final String UPDATE_CONTACT_KEY_REMOTE_VERIFICATION_STATE_METHOD = + "updateContactKeyRemoteVerificationState"; + + /** + * The method to invoke in order to remove a contact key. + */ + public static final String REMOVE_CONTACT_KEY_METHOD = "removeContactKey"; + + /** + * The method to invoke in order to add a new self key. + */ + public static final String UPDATE_OR_INSERT_SELF_KEY_METHOD = "updateOrInsertSelfKey"; + + /** + * The method to invoke in order to update a self key remote verification state. + */ + public static final String UPDATE_SELF_KEY_REMOTE_VERIFICATION_STATE_METHOD = + "updateSelfKeyRemoteVerificationState"; + + /** + * The method to invoke in order to retrieve a self key. + */ + public static final String GET_SELF_KEY_METHOD = "getSelfKey"; + + /** + * The method to invoke in order to retrieve all self keys. + */ + public static final String GET_ALL_SELF_KEYS_METHOD = "getAllSelfKeys"; + + /** + * The method to invoke in order to retrieve self keys that belong to the caller. + */ + public static final String GET_OWNER_SELF_KEYS_METHOD = "getOwnerSelfKeys"; + + /** + * The method to invoke in order to remove a new self key. + */ + public static final String REMOVE_SELF_KEY_METHOD = "removeSelfKey"; + + /** + * Key in the incoming Bundle for all the contact keys. + */ + public static final String KEY_CONTACT_KEYS = "key_contact_keys"; + + /** + * Key in the incoming Bundle for a single contact key. + */ + public static final String KEY_CONTACT_KEY = "key_contact_key"; + + /** + * Key in the incoming Bundle for a number of modified rows. + */ + public static final String KEY_UPDATED_ROWS = "key_updated_rows"; + } + + /** + * A parcelable class encapsulating other users' E2EE contact key. + */ + public static final class ContactKey implements Parcelable { + /** + * An app-specified identifier for the device for which the contact key can be used. + */ + private final String mDeviceId; + + /** + * An app-specified identifier for the account for which the contact key can be used. + * Usually a phone number. + */ + private final String mAccountId; + + /** + * Owner application package name. + */ + private final String mOwnerPackageName; + + /** + * Timestamp at which the key was updated. + */ + private final long mTimeUpdated; + + /** + * The raw bytes for the key. + */ + private final byte[] mKeyValue; + + /** + * Describes the local verification state for the key, for instance QR-code based + * verification. + */ + private final int mLocalVerificationState; + + /** + * Describes the remote verification state for the key, for instance through a key + * transparency server. + */ + private final int mRemoteVerificationState; + + /** + * The display name for the contact. + */ + private final String mDisplayName; + + /** + * The phone number as the user entered it. + */ + private final String mPhoneNumber; + + /** + * The email address. + */ + private final String mEmailAddress; + + /** + * @hide + */ + public ContactKey(@Nullable String deviceId, @NonNull String accountId, + @NonNull String ownerPackageName, long timeUpdated, @Nullable byte[] keyValue, + @VerificationState int localVerificationState, + @VerificationState int remoteVerificationState, + @Nullable String displayName, + @Nullable String phoneNumber, @Nullable String emailAddress) { + this.mDeviceId = deviceId; + this.mAccountId = accountId; + this.mOwnerPackageName = ownerPackageName; + this.mTimeUpdated = timeUpdated; + this.mKeyValue = keyValue == null ? null : Arrays.copyOf(keyValue, keyValue.length); + this.mLocalVerificationState = localVerificationState; + this.mRemoteVerificationState = remoteVerificationState; + this.mDisplayName = displayName; + this.mPhoneNumber = phoneNumber; + this.mEmailAddress = emailAddress; + } + + /** + * Gets the app-specified identifier for the device for which the contact key can be used. + * Returns null if the app doesn't have the required visibility into the contact key. + * + * @return An app-specified identifier for the device. + */ + @Nullable + public String getDeviceId() { + return mDeviceId; + } + + /** + * Gets the app-specified identifier for the account for which the contact key can be used. + * Usually a phone number. + * + * @return An app-specified identifier for the account. + */ + @NonNull + public String getAccountId() { + return mAccountId; + } + + /** + * Gets the owner application package name. + * + * @return The owner application package name. + */ + @NonNull + public String getOwnerPackageName() { + return mOwnerPackageName; + } + + /** + * Gets the timestamp at which the key was updated. Returns -1 if the app doesn't have the + * required visibility into the contact key. + * + * @return The timestamp at which the key was updated in the System.currentTimeMillis() + * base. + */ + public long getTimeUpdated() { + return mTimeUpdated; + } + + /** + * Gets the raw bytes for the key. + * Returns null if the app doesn't have the required visibility into the contact key. + * + * @return A copy of the raw bytes for the key. + */ + @Nullable + public byte[] getKeyValue() { + return mKeyValue == null ? null : Arrays.copyOf(mKeyValue, mKeyValue.length); + } + + /** + * Gets the local verification state for the key, for instance QR-code based verification. + * + * @return The local verification state for the key. + */ + public @VerificationState int getLocalVerificationState() { + return mLocalVerificationState; + } + + /** + * Gets the remote verification state for the key, for instance through a key transparency + * server. + * + * @return The remote verification state for the key. + */ + public @VerificationState int getRemoteVerificationState() { + return mRemoteVerificationState; + } + + /** + * Gets the display name for the contact. + * + * @return The display name for the contact. + */ + @Nullable + public String getDisplayName() { + return mDisplayName; + } + + /** + * Gets the phone number as the user entered it. + * + * @return The phone number as the user entered it. + */ + @Nullable + public String getPhoneNumber() { + return mPhoneNumber; + } + + /** + * Gets the email address. + * + * @return The email address. + */ + @Nullable + public String getEmailAddress() { + return mEmailAddress; + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceId, mAccountId, mOwnerPackageName, mTimeUpdated, + Arrays.hashCode(mKeyValue), mLocalVerificationState, mRemoteVerificationState, + mDisplayName, mPhoneNumber, mEmailAddress); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + + if (!(obj instanceof ContactKey toCompare)) { + return false; + } + + return Objects.equals(mDeviceId, toCompare.mDeviceId) + && Objects.equals(mAccountId, toCompare.mAccountId) + && Objects.equals(mOwnerPackageName, toCompare.mOwnerPackageName) + && mTimeUpdated == toCompare.mTimeUpdated + && Arrays.equals(mKeyValue, toCompare.mKeyValue) + && mLocalVerificationState == toCompare.mLocalVerificationState + && mRemoteVerificationState == toCompare.mRemoteVerificationState + && Objects.equals(mDisplayName, toCompare.mDisplayName) + && Objects.equals(mPhoneNumber, toCompare.mPhoneNumber) + && Objects.equals(mEmailAddress, toCompare.mEmailAddress); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mDeviceId); + dest.writeString8(mAccountId); + dest.writeString8(mOwnerPackageName); + dest.writeLong(mTimeUpdated); + dest.writeInt(mKeyValue != null ? mKeyValue.length : ARRAY_IS_NULL); + if (mKeyValue != null) { + dest.writeByteArray(mKeyValue); + } + dest.writeInt(mLocalVerificationState); + dest.writeInt(mRemoteVerificationState); + dest.writeString8(mDisplayName); + dest.writeString8(mPhoneNumber); + dest.writeString8(mEmailAddress); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<ContactKey> CREATOR = + new Creator<>() { + @Override + public ContactKey createFromParcel(Parcel source) { + String deviceId = source.readString8(); + String accountId = source.readString8(); + String ownerPackageName = source.readString8(); + long timeUpdated = source.readLong(); + int keyValueLength = source.readInt(); + byte[] keyValue; + if (keyValueLength > 0) { + keyValue = new byte[keyValueLength]; + source.readByteArray(keyValue); + } else { + keyValue = null; + } + int localVerificationState = source.readInt(); + int remoteVerificationState = source.readInt(); + String displayName = source.readString8(); + String number = source.readString8(); + String address = source.readString8(); + return new ContactKey(deviceId, accountId, ownerPackageName, + timeUpdated, keyValue, localVerificationState, + remoteVerificationState, displayName, number, address); + } + + @Override + public ContactKey[] newArray(int size) { + return new ContactKey[size]; + } + }; + } + + /** + * A parcelable class encapsulating self E2EE contact key. + */ + public static final class SelfKey implements Parcelable { + /** + * An app-specified identifier for the device for which the contact key can be used. + */ + private final String mDeviceId; + + /** + * An app-specified identifier for the account for which the contact key can be used. + * Usually a phone number. + */ + private final String mAccountId; + + /** + * Owner application package name. + */ + private final String mOwnerPackageName; + + /** + * Timestamp at which the key was updated. + */ + private final long mTimeUpdated; + + /** + * The raw bytes for the key. + */ + private final byte[] mKeyValue; + + + /** + * Describes the remote verification state for the key, for instance through a key + * transparency server. + */ + private final int mRemoteVerificationState; + + /** + * @hide + */ + public SelfKey(@Nullable String deviceId, @NonNull String accountId, + @NonNull String ownerPackageName, long timeUpdated, @Nullable byte[] keyValue, + @VerificationState int remoteVerificationState) { + this.mDeviceId = deviceId; + this.mAccountId = accountId; + this.mOwnerPackageName = ownerPackageName; + this.mTimeUpdated = timeUpdated; + this.mKeyValue = keyValue == null ? null : Arrays.copyOf(keyValue, keyValue.length); + this.mRemoteVerificationState = remoteVerificationState; + } + + /** + * Gets the app-specified identifier for the device for which the contact key can be used. + * Returns null if the app doesn't have the required visibility into the contact key. + * + * @return An app-specified identifier for the device. + */ + @Nullable + public String getDeviceId() { + return mDeviceId; + } + + /** + * Gets the app-specified identifier for the account for which the contact key can be used. + * Usually a phone number. + * + * @return An app-specified identifier for the device. + */ + @NonNull + public String getAccountId() { + return mAccountId; + } + + /** + * Gets the owner application package name. + * + * @return The owner application package name. + */ + @NonNull + public String getOwnerPackageName() { + return mOwnerPackageName; + } + + /** + * Gets the timestamp at which the key was updated. Returns -1 if the app doesn't have the + * required visibility into the contact key. + * + * @return The timestamp at which the key was updated in the System.currentTimeMillis() + * base. + */ + public long getTimeUpdated() { + return mTimeUpdated; + } + + /** + * Gets the raw bytes for the key. + * Returns null if the app doesn't have the required visibility into the contact key. + * + * @return A copy of the raw bytes for the key. + */ + @Nullable + public byte[] getKeyValue() { + return mKeyValue == null ? null : Arrays.copyOf(mKeyValue, mKeyValue.length); + } + + /** + * Gets the remote verification state for the key, for instance through a key transparency + * server. + * + * @return The remote verification state for the key. + */ + public @VerificationState int getRemoteVerificationState() { + return mRemoteVerificationState; + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceId, mAccountId, mOwnerPackageName, mTimeUpdated, + Arrays.hashCode(mKeyValue), mRemoteVerificationState); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj == this) return true; + + if (!(obj instanceof SelfKey toCompare)) { + return false; + } + + return Objects.equals(mDeviceId, toCompare.mDeviceId) + && Objects.equals(mAccountId, toCompare.mAccountId) + && Objects.equals(mOwnerPackageName, toCompare.mOwnerPackageName) + && mTimeUpdated == toCompare.mTimeUpdated + && Arrays.equals(mKeyValue, toCompare.mKeyValue) + && mRemoteVerificationState == toCompare.mRemoteVerificationState; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mDeviceId); + dest.writeString8(mAccountId); + dest.writeString8(mOwnerPackageName); + dest.writeLong(mTimeUpdated); + dest.writeInt(mKeyValue != null ? mKeyValue.length : ARRAY_IS_NULL); + if (mKeyValue != null) { + dest.writeByteArray(mKeyValue); + } + dest.writeInt(mRemoteVerificationState); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<SelfKey> CREATOR = + new Creator<>() { + @Override + public SelfKey createFromParcel(Parcel source) { + String deviceId = source.readString8(); + String accountId = source.readString8(); + String ownerPackageName = source.readString8(); + long timeUpdated = source.readLong(); + int keyValueLength = source.readInt(); + byte[] keyValue; + if (keyValueLength > 0) { + keyValue = new byte[keyValueLength]; + source.readByteArray(keyValue); + } else { + keyValue = null; + } + int remoteVerificationState = source.readInt(); + return new SelfKey(deviceId, accountId, ownerPackageName, + timeUpdated, keyValue, remoteVerificationState); + } + + @Override + public SelfKey[] newArray(int size) { + return new SelfKey[size]; + } + }; + } +} diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig index 3dd7692cd70d..0f12b1397c5b 100644 --- a/core/java/android/provider/flags.aconfig +++ b/core/java/android/provider/flags.aconfig @@ -6,3 +6,10 @@ flag { description: "Enable Settings.System.resetToDefault APIs." bug: "279083734" } + +flag { + name: "user_keys" + namespace: "privacy_infra_policy" + description: "This flag controls new E2EE contact keys API" + bug: "290696572" +} |