Merge "Support bulk capability exchange in UCE"
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java b/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java
new file mode 100644
index 0000000..7083631
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import static com.android.ims.rcs.uce.eab.EabControllerImpl.getCapabilityCacheExpiration;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.Telephony;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class EabBulkCapabilityUpdater {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private static final Uri USER_EAB_SETTING = Uri.withAppendedPath(Telephony.SimInfo.CONTENT_URI,
+ Telephony.SimInfo.COLUMN_IMS_RCS_UCE_ENABLED);
+ private static final int NUM_SECS_IN_DAY = 86400;
+
+ private final int mSubId;
+ private final Context mContext;
+ private final Handler mHandler;
+
+ private final AlarmManager.OnAlarmListener mCapabilityExpiredListener;
+ private final ContactChangedListener mContactProviderListener;
+ private final EabSettingsListener mEabSettingListener;
+ private final BroadcastReceiver mCarrierConfigChangedListener;
+ private final EabControllerImpl mEabControllerImpl;
+ private final EabContactSyncController mEabContactSyncController;
+
+ private UceController.UceControllerCallback mUceControllerCallback;
+ private List<Uri> mRefreshContactList;
+
+ private boolean mIsContactProviderListenerRegistered = false;
+ private boolean mIsEabSettingListenerRegistered = false;
+ private boolean mIsCarrierConfigListenerRegistered = false;
+ private boolean mIsCarrierConfigEnabled = false;
+
+ /**
+ * Listen capability expired intent. Only registered when
+ * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+ * capability exchange.
+ */
+ private class CapabilityExpiredListener implements AlarmManager.OnAlarmListener {
+ @Override
+ public void onAlarm() {
+ Log.d(TAG, "Capability expired.");
+ try {
+ List<Uri> expiredContactList = getExpiredContactList();
+ if (expiredContactList.size() > 0) {
+ mUceControllerCallback.refreshCapabilities(
+ getExpiredContactList(),
+ mRcsUceControllerCallback);
+ } else {
+ Log.d(TAG, "expiredContactList is empty.");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "CapabilityExpiredListener RemoteException", e);
+ }
+ }
+ }
+
+ /**
+ * Listen contact provider change. Only registered when
+ * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+ * capability exchange.
+ */
+ private class ContactChangedListener extends ContentObserver {
+ public ContactChangedListener(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Log.d(TAG, "Contact changed");
+ syncContactAndRefreshCapabilities();
+ }
+ }
+
+ /**
+ * Listen EAB settings change. Only registered when
+ * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+ * capability exchange.
+ */
+ private class EabSettingsListener extends ContentObserver {
+ public EabSettingsListener(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ boolean isUserEnableUce = isUserEnableUce();
+ Log.d(TAG, "EAB user setting changed: " + isUserEnableUce);
+ if (isUserEnableUce) {
+ mHandler.post(new SyncContactRunnable());
+ } else {
+ unRegisterContactProviderListener();
+ cancelTimeAlert(mContext);
+ }
+ }
+ }
+
+ /**
+ * Listen carrier config changed to prevent this instance created before carrier config loaded.
+ */
+ private class CarrierConfigChangedListener extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+ Log.d(TAG, "Carrier config changed. "
+ + "isCarrierConfigEnabled: " + mIsCarrierConfigEnabled
+ + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+ if (!mIsCarrierConfigEnabled && isSupportBulkCapabilityExchange) {
+ enableBulkCapability();
+ updateExpiredTimeAlert();
+ mIsCarrierConfigEnabled = true;
+ } else if (mIsCarrierConfigEnabled && !isSupportBulkCapabilityExchange) {
+ onDestroy();
+ }
+ }
+ }
+
+ private IRcsUceControllerCallback mRcsUceControllerCallback = new IRcsUceControllerCallback() {
+ @Override
+ public void onCapabilitiesReceived(List<RcsContactUceCapability> contactCapabilities) {
+ Log.d(TAG, "onCapabilitiesReceived");
+ mEabControllerImpl.saveCapabilities(contactCapabilities);
+ }
+
+ @Override
+ public void onComplete() {
+ Log.d(TAG, "onComplete");
+ }
+
+ @Override
+ public void onError(int errorCode, long retryAfterMilliseconds) {
+ Log.d(TAG, "Refresh capabilities failed. Error code: " + errorCode
+ + ", retryAfterMilliseconds: " + retryAfterMilliseconds);
+ if (retryAfterMilliseconds != 0) {
+ mHandler.postDelayed(new retryRunnable(), retryAfterMilliseconds);
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+ };
+
+ private class SyncContactRunnable implements Runnable {
+ @Override
+ public void run() {
+ Log.d(TAG, "Sync contact from contact provider");
+ syncContactAndRefreshCapabilities();
+ registerContactProviderListener();
+ registerEabUserSettingsListener();
+ }
+ }
+
+ /**
+ * Re-refresh capability if error happened.
+ */
+ private class retryRunnable implements Runnable {
+ @Override
+ public void run() {
+ Log.d(TAG, "Retry refreshCapabilities()");
+
+ try {
+ mUceControllerCallback.refreshCapabilities(
+ mRefreshContactList, mRcsUceControllerCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "refreshCapabilities RemoteException" , e);
+ }
+ }
+ }
+
+ public EabBulkCapabilityUpdater(Context context,
+ int subId,
+ EabControllerImpl eabControllerImpl,
+ EabContactSyncController eabContactSyncController,
+ UceController.UceControllerCallback uceControllerCallback,
+ Looper looper) {
+ mContext = context;
+ mSubId = subId;
+ mEabControllerImpl = eabControllerImpl;
+ mEabContactSyncController = eabContactSyncController;
+ mUceControllerCallback = uceControllerCallback;
+
+ mHandler = new Handler(looper);
+ mContactProviderListener = new ContactChangedListener(mHandler);
+ mEabSettingListener = new EabSettingsListener(mHandler);
+ mCapabilityExpiredListener = new CapabilityExpiredListener();
+ mCarrierConfigChangedListener = new CarrierConfigChangedListener();
+
+ Log.d(TAG, "create EabBulkCapabilityUpdater() subId: " + mSubId);
+
+ enableBulkCapability();
+ updateExpiredTimeAlert();
+ }
+
+ private void enableBulkCapability() {
+ boolean isUserEnableUce = isUserEnableUce();
+ boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+ Log.d(TAG, "isUserEnableUce: " + isUserEnableUce
+ + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+
+ if (isUserEnableUce && isSupportBulkCapabilityExchange) {
+ mHandler.post(new SyncContactRunnable());
+ mIsCarrierConfigEnabled = true;
+ } else if (!isUserEnableUce && isSupportBulkCapabilityExchange) {
+ registerEabUserSettingsListener();
+ mIsCarrierConfigEnabled = false;
+ } else {
+ registerCarrierConfigChanged();
+ Log.d(TAG, "Not support bulk capability exchange.");
+ }
+ }
+
+ private void syncContactAndRefreshCapabilities() {
+ mRefreshContactList = mEabContactSyncController.syncContactToEabProvider(mContext);
+ Log.d(TAG, "refresh contacts number: " + mRefreshContactList.size());
+
+ if (mUceControllerCallback == null) {
+ Log.d(TAG, "mUceControllerCallback is null.");
+ return;
+ }
+
+ try {
+ if (mRefreshContactList.size() > 0) {
+ mUceControllerCallback.refreshCapabilities(
+ mRefreshContactList, mRcsUceControllerCallback);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "mUceControllerCallback RemoteException.", e);
+ }
+ }
+
+ protected void updateExpiredTimeAlert() {
+ boolean isUserEnableUce = isUserEnableUce();
+ boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+ Log.d(TAG, " updateExpiredTimeAlert(), isUserEnableUce: " + isUserEnableUce
+ + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+
+ if (isUserEnableUce && isSupportBulkCapabilityExchange) {
+ long expiredTimestamp = getLeastExpiredTimestamp();
+ if (expiredTimestamp == Long.MAX_VALUE) {
+ Log.d(TAG, "Can't find min timestamp in eab provider");
+ return;
+ }
+ expiredTimestamp += getCapabilityCacheExpiration(mSubId);
+ Log.d(TAG, "set time alert at " + expiredTimestamp);
+ cancelTimeAlert(mContext);
+ setTimeAlert(mContext, expiredTimestamp);
+ }
+ }
+
+ private long getLeastExpiredTimestamp() {
+ String selection = "("
+ // Query presence timestamp
+ + EabProvider.EabCommonColumns.MECHANISM + "=" + CAPABILITY_MECHANISM_PRESENCE
+ + " AND "
+ + EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + " IS NOT NULL) "
+
+ // Query options timestamp
+ + " OR " + "(" + EabProvider.EabCommonColumns.MECHANISM + "="
+ + CAPABILITY_MECHANISM_OPTIONS + " AND "
+ + EabProvider.OptionsColumns.REQUEST_TIMESTAMP + " IS NOT NULL) "
+
+ // filter by sub id
+ + " AND " + EabProvider.EabCommonColumns.SUBSCRIPTION_ID + "=" + mSubId
+
+ // filter the contact that not come from contact provider
+ + " AND " + EabProvider.ContactColumns.RAW_CONTACT_ID + " IS NOT NULL "
+ + " AND " + EabProvider.ContactColumns.DATA_ID + " IS NOT NULL ";
+
+ long minTimestamp = Long.MAX_VALUE;
+ Cursor result = mContext.getContentResolver().query(EabProvider.ALL_DATA_URI, null,
+ selection,
+ null, null);
+
+ if (result != null) {
+ while (result.moveToNext()) {
+ int mechanism = result.getInt(
+ result.getColumnIndex(EabProvider.EabCommonColumns.MECHANISM));
+ long timestamp;
+ if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+ timestamp = result.getLong(result.getColumnIndex(
+ EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP));
+ } else {
+ timestamp = result.getLong(result.getColumnIndex(
+ EabProvider.OptionsColumns.REQUEST_TIMESTAMP));
+ }
+
+ if (timestamp < minTimestamp) {
+ minTimestamp = timestamp;
+ }
+ }
+ result.close();
+ } else {
+ Log.d(TAG, "getLeastExpiredTimestamp() cursor is null");
+ }
+ return minTimestamp;
+ }
+
+ private void setTimeAlert(Context context, long wakeupTimeMs) {
+ AlarmManager am = context.getSystemService(AlarmManager.class);
+
+ // To prevent all devices from sending requests to the server at the same time, add a jitter
+ // time (0 sec ~ 2 days) randomly.
+ int jitterTimeSec = (int) (Math.random() * (NUM_SECS_IN_DAY * 2));
+ Log.d(TAG, " setTimeAlert: " + wakeupTimeMs + ", jitterTimeSec: " + jitterTimeSec);
+ am.set(AlarmManager.RTC_WAKEUP,
+ (wakeupTimeMs * 1000) + jitterTimeSec,
+ TAG,
+ mCapabilityExpiredListener,
+ mHandler);
+ }
+
+ private void cancelTimeAlert(Context context) {
+ Log.d(TAG, "cancelTimeAlert.");
+ AlarmManager am = context.getSystemService(AlarmManager.class);
+ am.cancel(mCapabilityExpiredListener);
+ }
+
+ private boolean getBooleanCarrierConfig(String key, int subId) {
+ CarrierConfigManager mConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+ PersistableBundle b = null;
+ if (mConfigManager != null) {
+ b = mConfigManager.getConfigForSubId(subId);
+ }
+ if (b != null) {
+ return b.getBoolean(key);
+ } else {
+ Log.w(TAG, "getConfigForSubId(subId) is null. Return the default value of " + key);
+ return CarrierConfigManager.getDefaultConfig().getBoolean(key);
+ }
+ }
+
+ private boolean isUserEnableUce() {
+ ImsManager manager = mContext.getSystemService(ImsManager.class);
+ if (manager == null) {
+ Log.e(TAG, "ImsManager is null");
+ return false;
+ }
+ try {
+ ImsRcsManager rcsManager = manager.getImsRcsManager(mSubId);
+ return (rcsManager != null) && rcsManager.getUceAdapter().isUceSettingEnabled();
+ } catch (Exception e) {
+ Log.e(TAG, "hasUserEnabledUce: exception = " + e.getMessage());
+ }
+ return false;
+ }
+
+ private List<Uri> getExpiredContactList() {
+ List<Uri> refreshList = new ArrayList<>();
+ long expiredTime = (System.currentTimeMillis() / 1000)
+ + getCapabilityCacheExpiration(mSubId);
+ String selection = "("
+ + EabProvider.EabCommonColumns.MECHANISM + "=" + CAPABILITY_MECHANISM_PRESENCE
+ + " AND " + EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + "<"
+ + expiredTime + ")";
+ selection += " OR " + "(" + EabProvider.EabCommonColumns.MECHANISM + "="
+ + CAPABILITY_MECHANISM_OPTIONS + " AND "
+ + EabProvider.OptionsColumns.REQUEST_TIMESTAMP + "<" + expiredTime + ")";
+
+ Cursor result = mContext.getContentResolver().query(EabProvider.ALL_DATA_URI, null,
+ selection,
+ null, null);
+ while (result.moveToNext()) {
+ String phoneNumber = result.getString(
+ result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER));
+ refreshList.add(Uri.parse(phoneNumber));
+ }
+ result.close();
+ return refreshList;
+ }
+
+ protected void onDestroy() {
+ Log.d(TAG, "onDestroy");
+ cancelTimeAlert(mContext);
+ unRegisterContactProviderListener();
+ unRegisterEabUserSettings();
+ unRegisterCarrierConfigChanged();
+ }
+
+ private void registerContactProviderListener() {
+ Log.d(TAG, "registerContactProviderListener");
+ mIsContactProviderListenerRegistered = true;
+ mContext.getContentResolver().registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI,
+ true,
+ mContactProviderListener);
+ }
+
+ private void registerEabUserSettingsListener() {
+ Log.d(TAG, "registerEabUserSettingsListener");
+ mIsEabSettingListenerRegistered = true;
+ mContext.getContentResolver().registerContentObserver(
+ USER_EAB_SETTING,
+ true,
+ mEabSettingListener);
+ }
+
+ private void registerCarrierConfigChanged() {
+ Log.d(TAG, "registerCarrierConfigChanged");
+ mIsCarrierConfigListenerRegistered = true;
+ IntentFilter FILTER_CARRIER_CONFIG_CHANGED =
+ new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+ mContext.registerReceiver(mCarrierConfigChangedListener, FILTER_CARRIER_CONFIG_CHANGED);
+ }
+
+ private void unRegisterContactProviderListener() {
+ Log.d(TAG, "unRegisterContactProviderListener");
+ if (mIsContactProviderListenerRegistered) {
+ mIsContactProviderListenerRegistered = false;
+ mContext.getContentResolver().unregisterContentObserver(mContactProviderListener);
+ }
+ }
+
+ private void unRegisterEabUserSettings() {
+ Log.d(TAG, "unRegisterEabUserSettings");
+ if (mIsEabSettingListenerRegistered) {
+ mIsEabSettingListenerRegistered = false;
+ mContext.getContentResolver().unregisterContentObserver(mEabSettingListener);
+ }
+ }
+
+ private void unRegisterCarrierConfigChanged() {
+ Log.d(TAG, "unregisterCarrierConfigChanged");
+ if (mIsCarrierConfigListenerRegistered) {
+ mIsCarrierConfigListenerRegistered = false;
+ mContext.unregisterReceiver(mCarrierConfigChangedListener);
+ }
+ }
+
+ public void setUceRequestCallback(UceController.UceControllerCallback uceControllerCallback) {
+ mUceControllerCallback = uceControllerCallback;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java b/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java
new file mode 100644
index 0000000..e9bb9ec
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ims.rcs.uce.eab;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Sync the contacts from Contact Provider to EAB Provider
+ */
+public class EabContactSyncController {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private static final int NOT_INIT_LAST_UPDATED_TIME = -1;
+ private static final String LAST_UPDATED_TIME_KEY = "eab_last_updated_time";
+
+ /**
+ * Sync contact from Contact provider to EAB provider. There are 4 kinds of cases need to be
+ * handled when received the contact db changed:
+ *
+ * 1. Contact deleted
+ * 2. Delete the phone number in the contact
+ * 3. Update the phone number
+ * 4. Add a new contact and add phone number
+ *
+ * @return The contacts that need to refresh
+ */
+ @VisibleForTesting
+ public List<Uri> syncContactToEabProvider(Context context) {
+ Log.d(TAG, "syncContactToEabProvider");
+ List<Uri> refreshContacts = null;
+ StringBuilder selection = new StringBuilder();
+ String[] selectionArgs = null;
+
+ // Get the last update timestamp from shared preference.
+ long lastUpdatedTimeStamp = getLastUpdatedTime(context);
+ if (lastUpdatedTimeStamp != -1) {
+ selection.append(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + ">?");
+ selectionArgs = new String[]{String.valueOf(lastUpdatedTimeStamp)};
+ }
+
+ // Contact deleted cases (case 1)
+ handleContactDeletedCase(context, lastUpdatedTimeStamp);
+
+ // Query the contacts that have not been synchronized to eab contact table.
+ Cursor updatedContact = context.getContentResolver().query(
+ ContactsContract.Data.CONTENT_URI,
+ null,
+ selection.toString(),
+ selectionArgs,
+ null);
+
+ if (updatedContact != null) {
+ Log.d(TAG, "Contact changed count: " + updatedContact.getCount());
+
+ // Delete the EAB phone number that not in contact provider (case 2). Updated phone
+ // number(case 3) also delete in here and re-insert in next step.
+ handlePhoneNumberDeletedCase(context, updatedContact);
+
+ // Insert the phone number that not in EAB provider (case 3 and case 4)
+ refreshContacts = handlePhoneNumberInsertedCase(context, updatedContact);
+
+ // Update the last update time in shared preference
+ if (updatedContact.getCount() > 0) {
+ long maxTimestamp = findMaxTimestamp(updatedContact);
+ if (maxTimestamp != Long.MIN_VALUE) {
+ setLastUpdatedTime(context, maxTimestamp);
+ }
+ }
+ updatedContact.close();
+ } else {
+ Log.e(TAG, "Cursor is null.");
+ }
+ return refreshContacts;
+ }
+
+ /**
+ * Delete the phone numbers that contact has been deleted in contact provider. Query based on
+ * {@link ContactsContract.DeletedContacts#CONTENT_URI} to know which contact has been removed.
+ *
+ * @param timeStamp last updated timestamp
+ */
+ private void handleContactDeletedCase(Context context, long timeStamp) {
+ String selection = "";
+ if (timeStamp != -1) {
+ selection =
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">" + timeStamp;
+ }
+
+ Cursor cursor = context.getContentResolver().query(
+ ContactsContract.DeletedContacts.CONTENT_URI,
+ new String[]{ContactsContract.DeletedContacts.CONTACT_ID,
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP},
+ selection,
+ null,
+ null);
+
+ if (cursor == null) {
+ Log.d(TAG, "handleContactDeletedCase() cursor is null.");
+ return;
+ }
+
+ Log.d(TAG, "(Case 1) The count of contact that need to be deleted: "
+ + cursor.getCount());
+
+ StringBuilder deleteClause = new StringBuilder();
+ while (cursor.moveToNext()) {
+ if (deleteClause.length() > 0) {
+ deleteClause.append(" OR ");
+ }
+
+ String contactId = cursor.getString(cursor.getColumnIndex(
+ ContactsContract.DeletedContacts.CONTACT_ID));
+ deleteClause.append(EabProvider.ContactColumns.CONTACT_ID + "=" + contactId);
+ }
+
+ if (deleteClause.toString().length() > 0) {
+ int number = context.getContentResolver().delete(
+ EabProvider.CONTACT_URI,
+ deleteClause.toString(),
+ null);
+ Log.d(TAG, "(Case 1) Deleted contact count=" + number);
+ }
+ }
+
+ /**
+ * Delete phone numbers that have been deleted in the contact provider. There is no API to get
+ * deleted phone numbers easily, so check all updated contact's phone number and delete the
+ * phone number. It will also delete the phone number that has been changed.
+ */
+ private void handlePhoneNumberDeletedCase(Context context, Cursor cursor) {
+ // The map represent which contacts have which numbers.
+ Map<String, List<String>> phoneNumberMap = new HashMap<>();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ String rawContactId = cursor.getString(
+ cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID));
+ String number = cursor.getString(
+ cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
+
+ if (phoneNumberMap.containsKey(rawContactId)) {
+ phoneNumberMap.get(rawContactId).add(number);
+ } else {
+ List<String> phoneNumberList = new ArrayList<>();
+ phoneNumberList.add(number);
+ phoneNumberMap.put(rawContactId, phoneNumberList);
+ }
+ }
+
+ // Build a SQL statement that delete the phone number not exist in contact provider.
+ // For example:
+ // raw_contact_id = 1 AND phone_number NOT IN (12345, 23456)
+ StringBuilder deleteClause = new StringBuilder();
+ List<String> deleteClauseArgs = new ArrayList<>();
+ for (Map.Entry<String, List<String>> entry : phoneNumberMap.entrySet()) {
+ String rawContactId = entry.getKey();
+ List<String> phoneNumberList = entry.getValue();
+
+ if (deleteClause.length() > 0) {
+ deleteClause.append(" OR ");
+ }
+
+ deleteClause.append("(" + EabProvider.ContactColumns.RAW_CONTACT_ID + "=? ");
+ deleteClauseArgs.add(rawContactId);
+
+ if (phoneNumberList.size() > 0) {
+ String argsList = phoneNumberList.stream()
+ .map(s -> "?")
+ .collect(Collectors.joining(", "));
+ deleteClause.append(" AND "
+ + EabProvider.ContactColumns.PHONE_NUMBER
+ + " NOT IN (" + argsList + "))");
+ deleteClauseArgs.addAll(phoneNumberList);
+ } else {
+ deleteClause.append(")");
+ }
+ }
+
+ if (deleteClause.length() > 1) {
+ int number = context.getContentResolver().delete(
+ EabProvider.CONTACT_URI,
+ deleteClause.toString(),
+ deleteClauseArgs.toArray(new String[0]));
+ Log.d(TAG, "(Case 2, 3) handlePhoneNumberDeletedCase number count= " + number);
+ } else {
+ Log.d(TAG, "(Case 2, 3) handlePhoneNumberDeletedCase number count is empty.");
+ }
+ }
+
+ /**
+ * Insert new phone number.
+ *
+ * @param contactCursor the result of updated contact
+ * @return the contacts that need to refresh
+ */
+ private List<Uri> handlePhoneNumberInsertedCase(Context context,
+ Cursor contactCursor) {
+ List<Uri> refreshContacts = new ArrayList<>();
+ List<ContentValues> allContactData = new ArrayList<>();
+ contactCursor.moveToPosition(-1);
+
+ // Query all of contacts that store in eab provider
+ Cursor eabContact = context.getContentResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ EabProvider.ContactColumns.DATA_ID + " IS NOT NULL",
+ null,
+ EabProvider.ContactColumns.DATA_ID);
+
+ while (contactCursor.moveToNext()) {
+ String contactId = contactCursor.getString(contactCursor.getColumnIndex(
+ ContactsContract.Data.CONTACT_ID));
+ String rawContactId = contactCursor.getString(contactCursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID));
+ String dataId = contactCursor.getString(
+ contactCursor.getColumnIndex(ContactsContract.Data._ID));
+ String number = contactCursor.getString(
+ contactCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
+ String mimeType = contactCursor.getString(
+ contactCursor.getColumnIndex(ContactsContract.Data.MIMETYPE));
+
+
+ if (!mimeType.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ continue;
+ }
+
+ int index = searchDataIdIndex(eabContact, Integer.parseInt(dataId));
+ if (index == -1) {
+ Log.d(TAG, "Data id does not exist. Insert phone number into EAB db.");
+ refreshContacts.add(Uri.parse(number));
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+ data.put(EabProvider.ContactColumns.DATA_ID, dataId);
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId);
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, number);
+ allContactData.add(data);
+ }
+ }
+
+ // Insert contacts at once
+ int result = context.getContentResolver().bulkInsert(
+ EabProvider.CONTACT_URI,
+ allContactData.toArray(new ContentValues[0]));
+ Log.d(TAG, "(Case 3, 4) Phone number insert count: " + result);
+ return refreshContacts;
+ }
+
+ /**
+ * Binary search the target data_id in the cursor.
+ *
+ * @param cursor EabProvider contact which sorted by
+ * {@link EabProvider.ContactColumns#DATA_ID}
+ * @param targetDataId the data_id to search for
+ * @return the index of cursor
+ */
+ private int searchDataIdIndex(Cursor cursor, int targetDataId) {
+ int start = 0;
+ int end = cursor.getCount() - 1;
+
+ while (start <= end) {
+ int position = (start + end) >>> 1;
+ cursor.moveToPosition(position);
+ int dataId = cursor.getInt(cursor.getColumnIndex(EabProvider.ContactColumns.DATA_ID));
+
+ if (dataId > targetDataId) {
+ end = position - 1;
+ } else if (dataId < targetDataId) {
+ start = position + 1;
+ } else {
+ return position;
+ }
+ }
+ return -1;
+ }
+
+
+ private long findMaxTimestamp(Cursor cursor) {
+ long maxTimestamp = Long.MIN_VALUE;
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ long lastUpdatedTimeStamp = cursor.getLong(cursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP));
+ Log.d(TAG, lastUpdatedTimeStamp + " " + maxTimestamp);
+ if (lastUpdatedTimeStamp > maxTimestamp) {
+ maxTimestamp = lastUpdatedTimeStamp;
+ }
+ }
+ return maxTimestamp;
+ }
+
+ private void setLastUpdatedTime(Context context, long timestamp) {
+ Log.d(TAG, "setLastUpdatedTime: " + timestamp);
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ sharedPreferences.edit().putLong(LAST_UPDATED_TIME_KEY, timestamp).apply();
+ }
+
+ private long getLastUpdatedTime(Context context) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ return sharedPreferences.getLong(LAST_UPDATED_TIME_KEY, NOT_INIT_LAST_UPDATED_TIME);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java b/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
index 27c4218..0cffaf9 100644
--- a/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
+++ b/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
@@ -57,15 +57,20 @@
private final Context mContext;
private final int mSubId;
+ private final EabBulkCapabilityUpdater mEabBulkCapabilityUpdater;
+
private UceControllerCallback mUceControllerCallback;
- private final Looper mLooper;
private volatile boolean mIsSetDestroyedFlag = false;
public EabControllerImpl(Context context, int subId, UceControllerCallback c, Looper looper) {
mContext = context;
mSubId = subId;
mUceControllerCallback = c;
- mLooper = looper;
+ mEabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(mContext, mSubId,
+ this,
+ new EabContactSyncController(),
+ mUceControllerCallback,
+ looper);
}
@Override
@@ -80,6 +85,7 @@
public void onDestroy() {
Log.d(TAG, "onDestroy");
mIsSetDestroyedFlag = true;
+ mEabBulkCapabilityUpdater.onDestroy();
}
/**
@@ -93,6 +99,7 @@
return;
}
mUceControllerCallback = c;
+ mEabBulkCapabilityUpdater.setUceRequestCallback(c);
}
/**
@@ -148,7 +155,8 @@
for (RcsContactUceCapability capability : contactCapabilities) {
String phoneNumber = getNumberFromUri(capability.getContactUri());
Cursor c = mContext.getContentResolver().query(
- EabProvider.CONTACT_URI, null, EabProvider.ContactColumns.PHONE_NUMBER + "=?",
+ EabProvider.CONTACT_URI, null,
+ EabProvider.ContactColumns.PHONE_NUMBER + "=?",
new String[]{phoneNumber}, null);
if (c != null && c.moveToNext()) {
@@ -176,6 +184,8 @@
c.close();
}
}
+
+ mEabBulkCapabilityUpdater.updateExpiredTimeAlert();
}
private List<EabCapabilityResult> generateDestroyedResult(List<Uri> contactUri) {
@@ -335,7 +345,7 @@
if (requestTimeStamp != null) {
Instant expiredTimestamp = Instant
.ofEpochSecond(Long.parseLong(requestTimeStamp))
- .plus(getCapabilityCacheExpiration(), ChronoUnit.SECONDS);
+ .plus(getCapabilityCacheExpiration(mSubId), ChronoUnit.SECONDS);
expired = expiredTimestamp.isBefore(Instant.now());
Log.d(TAG, "Capability expiredTimestamp: "
+ expiredTimestamp.getEpochSecond() + ", expired:" + expired);
@@ -352,7 +362,7 @@
if (requestTimeStamp != null) {
Instant expiredTimestamp = Instant
.ofEpochSecond(Long.parseLong(requestTimeStamp))
- .plus(getAvailabilityCacheExpiration(), ChronoUnit.SECONDS);
+ .plus(getAvailabilityCacheExpiration(mSubId), ChronoUnit.SECONDS);
expired = expiredTimestamp.isBefore(Instant.now());
Log.d(TAG, "Availability insertedTimestamp: "
+ expiredTimestamp.getEpochSecond() + ", expired:" + expired);
@@ -375,10 +385,10 @@
return expiredTimestamp;
}
- private long getCapabilityCacheExpiration() {
+ protected static long getCapabilityCacheExpiration(int subId) {
long value = -1;
try {
- ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(mSubId);
+ ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
value = pm.getProvisioningIntValue(
ProvisioningManager.KEY_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC);
} catch (Exception ex) {
@@ -392,10 +402,10 @@
return value;
}
- private long getAvailabilityCacheExpiration() {
+ protected static long getAvailabilityCacheExpiration(int subId) {
long value = -1;
try {
- ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(mSubId);
+ ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
value = pm.getProvisioningIntValue(
ProvisioningManager.KEY_RCS_AVAILABILITY_CACHE_EXPIRATION_SEC);
} catch (Exception ex) {
@@ -424,10 +434,13 @@
new String[]{String.valueOf(id)}, null);
if (c != null && c.getCount() > 0) {
- int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
- mContext.getContentResolver().delete(
- EabProvider.PRESENCE_URI, EabProvider.PresenceTupleColumns.EAB_COMMON_ID + "=?",
- new String[]{String.valueOf(commonId)});
+ while(c.moveToNext()) {
+ int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
+ mContext.getContentResolver().delete(
+ EabProvider.PRESENCE_URI,
+ EabProvider.PresenceTupleColumns.EAB_COMMON_ID + "=?",
+ new String[]{String.valueOf(commonId)});
+ }
}
if (c != null) {
@@ -508,10 +521,13 @@
new String[]{String.valueOf(contactId)}, null);
if (c != null && c.getCount() > 0) {
- int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
- mContext.getContentResolver().delete(
- EabProvider.OPTIONS_URI, EabProvider.OptionsColumns.EAB_COMMON_ID + "=?",
- new String[]{String.valueOf(commonId)});
+ while(c.moveToNext()) {
+ int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
+ mContext.getContentResolver().delete(
+ EabProvider.OPTIONS_URI,
+ EabProvider.OptionsColumns.EAB_COMMON_ID + "=?",
+ new String[]{String.valueOf(commonId)});
+ }
}
if (c != null) {
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabProvider.java b/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
index 3d509ee..b11004e 100644
--- a/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
+++ b/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
@@ -16,6 +16,11 @@
package com.android.ims.rcs.uce.eab;
+import static android.content.ContentResolver.NOTIFY_DELETE;
+import static android.content.ContentResolver.NOTIFY_INSERT;
+import static android.content.ContentResolver.NOTIFY_SYNC_TO_NETWORK;
+import static android.content.ContentResolver.NOTIFY_UPDATE;
+
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
@@ -25,6 +30,7 @@
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
+import android.os.UserHandle;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
@@ -77,7 +83,7 @@
public static final String AUTHORITY = "eab";
private static final String TAG = "EabProvider";
- private static final int DATABASE_VERSION = 1;
+ private static final int DATABASE_VERSION = 2;
private static final String EAB_CONTACT_TABLE_NAME = "eab_contact";
private static final String EAB_COMMON_TABLE_NAME = "eab_common";
@@ -137,6 +143,15 @@
/**
* The ID of contact that store in contact provider. It refer to the
+ * {@link android.provider.ContactsContract.Data#CONTACT_ID}. If the phone number not in
+ * contact provider, the value should be null.
+ *
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CONTACT_ID = "contact_id";
+
+ /**
+ * The ID of contact that store in contact provider. It refer to the
* {@link android.provider.ContactsContract.Data#RAW_CONTACT_ID}. If the phone number not in
* contact provider, the value should be null.
*
@@ -328,6 +343,7 @@
+ " ("
+ ContactColumns._ID + " INTEGER PRIMARY KEY, "
+ ContactColumns.PHONE_NUMBER + " Text DEFAULT NULL, "
+ + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1, "
+ ContactColumns.RAW_CONTACT_ID + " INTEGER DEFAULT -1, "
+ ContactColumns.DATA_ID + " INTEGER DEFAULT -1, "
+ "UNIQUE (" + TextUtils.join(", ", CONTACT_UNIQUE_FIELDS) + ")"
@@ -389,6 +405,12 @@
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
Log.d(TAG, "DB upgrade from " + oldVersion + " to " + newVersion);
+
+ if (oldVersion < 2) {
+ sqLiteDatabase.execSQL("ALTER TABLE " + EAB_CONTACT_TABLE_NAME + " ADD COLUMN "
+ + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1;");
+ oldVersion = 2;
+ }
}
}
@@ -479,7 +501,7 @@
Log.d(TAG, "Query failed. Not support URL.");
return null;
}
- return qb.query(db, projection, selection, selectionArgs, null, null, null);
+ return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
}
@Override
@@ -506,9 +528,13 @@
result = db.insertWithOnConflict(tableName, null, contentValues,
SQLiteDatabase.CONFLICT_REPLACE);
Log.d(TAG, "Insert uri: " + match + " ID: " + result);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
+ }
} else {
Log.d(TAG, "Insert. Not support URI.");
}
+
return Uri.withAppendedPath(uri, String.valueOf(result));
}
@@ -552,6 +578,9 @@
} finally {
db.endTransaction();
}
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
+ }
Log.d(TAG, "bulkInsert uri: " + match + " count: " + result);
return result;
}
@@ -578,6 +607,9 @@
}
if (!TextUtils.isEmpty(tableName)) {
result = db.delete(tableName, selection, selectionArgs);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_DELETE);
+ }
Log.d(TAG, "Delete uri: " + match + " result: " + result);
} else {
Log.d(TAG, "Delete. Not support URI.");
@@ -609,6 +641,9 @@
if (!TextUtils.isEmpty(tableName)) {
result = db.updateWithOnConflict(tableName, contentValues,
selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_UPDATE);
+ }
Log.d(TAG, "Update uri: " + match + " result: " + result);
} else {
Log.d(TAG, "Update. Not support URI.");
@@ -623,13 +658,11 @@
@VisibleForTesting
public SQLiteDatabase getWritableDatabase() {
- Log.d(TAG, "getWritableDatabase");
return mOpenHelper.getWritableDatabase();
}
@VisibleForTesting
public SQLiteDatabase getReadableDatabase() {
- Log.d(TAG, "getReadableDatabase");
return mOpenHelper.getReadableDatabase();
}
}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 706035a..88831aa 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,6 +18,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ims.tests">
+ <!-- For EabBulkCapabilityUpdaterTest, EabBulkCapabilityUpdater will register content
+ observer to contact provider but currently there is no better way to mock contact provider
+ (registerContentObserver() is final), so require the read_contacts permission to test APK.-->
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
<application android:label="@string/app_name">
<uses-library android:name="android.test.runner" />
</application>
diff --git a/tests/src/com/android/ims/ContextFixture.java b/tests/src/com/android/ims/ContextFixture.java
index c11a71a..5cbef29 100644
--- a/tests/src/com/android/ims/ContextFixture.java
+++ b/tests/src/com/android/ims/ContextFixture.java
@@ -37,6 +37,7 @@
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsManager;
import android.test.mock.MockContentResolver;
import android.test.mock.MockContext;
@@ -50,9 +51,11 @@
private final Context mContext = spy(new FakeContext());
private final TelephonyManager mTelephonyManager = mock(TelephonyManager.class);
+ private final ConnectivityManager mConnectivityManager = mock(ConnectivityManager.class);
private final CarrierConfigManager mCarrierConfigManager = mock(CarrierConfigManager.class);
private final PackageManager mPackageManager = mock(PackageManager.class);
private final SubscriptionManager mSubscriptionManager = mock(SubscriptionManager.class);
+ private final ImsManager mImsManager = mock(ImsManager.class);
private final Resources mResources = mock(Resources.class);
private final PersistableBundle mBundle = new PersistableBundle();
@@ -91,8 +94,12 @@
return mTelephonyManager;
case Context.CARRIER_CONFIG_SERVICE:
return mCarrierConfigManager;
+ case Context.CONNECTIVITY_SERVICE:
+ return mConnectivityManager;
case Context.TELEPHONY_SUBSCRIPTION_SERVICE:
return mSubscriptionManager;
+ case Context.TELEPHONY_IMS_SERVICE:
+ return mImsManager;
default:
return null;
}
@@ -108,6 +115,10 @@
return Context.CONNECTIVITY_SERVICE;
} else if (serviceClass == TelephonyManager.class) {
return Context.TELEPHONY_SERVICE;
+ } else if (serviceClass == ImsManager.class) {
+ return Context.TELEPHONY_IMS_SERVICE;
+ } else if (serviceClass == CarrierConfigManager.class) {
+ return Context.CARRIER_CONFIG_SERVICE;
}
return super.getSystemServiceName(serviceClass);
}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java
new file mode 100644
index 0000000..7a52a40
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ims.rcs.uce.eab;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EabBulkCapabilityUpdaterTest extends ImsTestBase {
+
+ private final int mSubId = 1;
+ @Mock
+ private UceController.UceControllerCallback mMockUceControllerCallback;
+ @Mock
+ private EabControllerImpl mMockEabControllerImpl;
+ @Mock
+ private ImsRcsManager mImsRcsManager;
+ @Mock
+ private RcsUceAdapter mRcsUceAdapter;
+ @Mock
+ private SharedPreferences mSharedPreferences;
+ @Mock
+ private SharedPreferences.Editor mSharedPreferencesEditor;
+ @Mock
+ private EabContactSyncController mEabContactSyncController;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+ doReturn(0L).when(mSharedPreferences).getLong(anyString(), anyInt());
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferences).edit();
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferencesEditor).putLong(anyString(),
+ anyLong());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testRefreshCapabilities() throws Exception {
+ // mock user settings
+ mockUceUserSettings(true);
+ mockBulkCapabilityCarrierConfig(true);
+ // mock expired contact list
+ List<Uri> expiredContactList = new ArrayList<>();
+ expiredContactList.add(Uri.parse("test"));
+ doReturn(expiredContactList)
+ .when(mEabContactSyncController)
+ .syncContactToEabProvider(any());
+
+ EabBulkCapabilityUpdater eabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(
+ mContext,
+ mSubId,
+ mMockEabControllerImpl,
+ mEabContactSyncController,
+ mMockUceControllerCallback,
+ Looper.getMainLooper());
+
+
+ verify(mMockUceControllerCallback).refreshCapabilities(
+ anyList(),
+ any(IRcsUceControllerCallback.class));
+ }
+
+ @Test
+ public void testUceSettingsDisabled() throws Exception {
+ // mock user settings
+ mockUceUserSettings(false);
+ mockBulkCapabilityCarrierConfig(true);
+ // mock expired contact list
+ List<Uri> expiredContactList = new ArrayList<>();
+ expiredContactList.add(Uri.parse("test"));
+ doReturn(expiredContactList)
+ .when(mEabContactSyncController)
+ .syncContactToEabProvider(any());
+
+ EabBulkCapabilityUpdater eabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(
+ mContext,
+ mSubId,
+ mMockEabControllerImpl,
+ mEabContactSyncController,
+ mMockUceControllerCallback,
+ Looper.getMainLooper());
+
+ verify(mMockUceControllerCallback, never()).refreshCapabilities(
+ any(),
+ any(IRcsUceControllerCallback.class));
+ }
+
+ @Test
+ public void testCarrierConfigDisabled() throws Exception {
+ // mock user settings
+ mockUceUserSettings(true);
+ mockBulkCapabilityCarrierConfig(false);
+ // mock expired contact list
+ List<Uri> expiredContactList = new ArrayList<>();
+ expiredContactList.add(Uri.parse("test"));
+ doReturn(expiredContactList)
+ .when(mEabContactSyncController)
+ .syncContactToEabProvider(any());
+
+ EabBulkCapabilityUpdater eabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(
+ mContext,
+ mSubId,
+ mMockEabControllerImpl,
+ mEabContactSyncController,
+ mMockUceControllerCallback,
+ Looper.getMainLooper());
+
+ verify(mMockUceControllerCallback, never()).refreshCapabilities(
+ anyList(),
+ any(IRcsUceControllerCallback.class));
+ }
+
+ private void mockBulkCapabilityCarrierConfig(boolean isEnabled) {
+ PersistableBundle persistableBundle = new PersistableBundle();
+ persistableBundle.putBoolean(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, isEnabled);
+ CarrierConfigManager carrierConfigManager =
+ mContext.getSystemService(CarrierConfigManager.class);
+ doReturn(persistableBundle).when(carrierConfigManager).getConfigForSubId(anyInt());
+ }
+
+ private void mockUceUserSettings(boolean isEnabled) throws ImsException {
+ // mock uce user settings
+ ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+ doReturn(mImsRcsManager).when(imsManager).getImsRcsManager(eq(mSubId));
+ doReturn(mRcsUceAdapter).when(mImsRcsManager).getUceAdapter();
+ doReturn(isEnabled).when(mRcsUceAdapter).isUceSettingEnabled();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java
new file mode 100644
index 0000000..b500629
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ims.rcs.uce.eab;
+
+import static android.provider.ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.SharedPreferences;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.test.mock.MockContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.rule.provider.ProviderTestRule;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class EabContactSyncControllerTest extends ImsTestBase {
+ private static final String TAG = "EabContactDataSyncServiceTest";
+
+ FakeContactProvider mFakeContactProvider = new FakeContactProvider();
+
+ @Rule
+ public ProviderTestRule mProviderTestRule = new ProviderTestRule.Builder(
+ EabProvider.class, EabProvider.AUTHORITY).build();
+
+ @Mock private SharedPreferences mSharedPreferences;
+ @Mock private SharedPreferences.Editor mSharedPreferencesEditor;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockContentResolver mockContentResolver =
+ (MockContentResolver) mProviderTestRule.getResolver();
+ ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.authority = ContactsContract.AUTHORITY;
+ mFakeContactProvider.attachInfo(mContext, providerInfo);
+ mockContentResolver.addProvider(providerInfo.authority, mFakeContactProvider);
+ doReturn("com.android.phone.tests").when(mContext).getPackageName();
+
+ doReturn(mProviderTestRule.getResolver()).when(mContext).getContentResolver();
+
+ doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+ doReturn(0L).when(mSharedPreferences).getLong(anyString(), anyInt());
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferences).edit();
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferencesEditor).putLong(anyString(),
+ anyLong());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mFakeContactProvider.clearData();
+ mContext.getContentResolver().delete(EabProvider.CONTACT_URI, null, null);
+ }
+
+ @Test
+ public void testContactDeletedCase() {
+ insertContactToEabProvider(1, 2, 3, "123456");
+ insertDeletedContactToContactProvider(1, 1);
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null);
+ assertEquals(0, result.getCount());
+ }
+
+ @Test
+ public void testMultipleContactsDeletedCase() {
+ // Insert 3 contacts in EabProvider
+ insertContactToEabProvider(1, 1, 1, "123456");
+ insertContactToEabProvider(2, 2, 2, "1234567");
+ insertContactToEabProvider(3, 3, 3, "12345678");
+ // Insert 2 deleted contacts
+ insertDeletedContactToContactProvider(1, 1);
+ insertDeletedContactToContactProvider(2, 1);
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ // Make sure only 1 contact in Eab DB
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null);
+ assertEquals(1, result.getCount());
+ }
+
+ @Test
+ public void testPhoneNumberDeletedCase() {
+ insertContactToEabProvider(1, 1, 2, "123456");
+ insertContactToEabProvider(1, 1, 3, "1234567");
+ insertContactToEabProvider(1, 1, 4, "12345678");
+ // Delete phone number 12345678
+ insertContactToContactProvider(1, 1, 2, "123456");
+ insertContactToContactProvider(1, 1, 3, "1234567");
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null);
+ assertEquals(2, result.getCount());
+ }
+
+ @Test
+ public void testPhoneNumberUpdatedCase() {
+ insertContactToEabProvider(1, 1, 2, "123456");
+ insertContactToEabProvider(1, 1, 3, "1234567");
+ insertContactToEabProvider(1, 1, 4, "12345678");
+ // Update phone number to 1,2,3
+ insertContactToContactProvider(1, 1, 2, "1");
+ insertContactToContactProvider(1, 1, 3, "2");
+ insertContactToContactProvider(1, 1, 4, "3");
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null,
+ EabProvider.ContactColumns.DATA_ID);
+ result.moveToFirst();
+ assertEquals(3, result.getCount());
+ assertEquals("1",
+ result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+ result.moveToNext();
+ assertEquals("2",
+ result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+ result.moveToNext();
+ assertEquals("3",
+ result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+ }
+
+ private void insertDeletedContactToContactProvider(int contactId, int timestamp) {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.DeletedContacts.CONTACT_ID, contactId);
+ values.put(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, timestamp);
+ mContext.getContentResolver().insert(
+ ContactsContract.DeletedContacts.CONTENT_URI, values);
+ }
+
+ private void insertContactToContactProvider(
+ int contactId, int rawContactId, int dataId, String number) {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.Data._ID, dataId);
+ values.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+ values.put(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, rawContactId);
+ values.put(ContactsContract.Data.MIMETYPE, CONTENT_ITEM_TYPE);
+ values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, number);
+ values.put(ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP, 1);
+
+ mContext.getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
+ }
+
+ private void insertContactToEabProvider(int contactId,
+ int rawContactId, int dataId, String phoneNumber) {
+ ContentValues values = new ContentValues();
+ values.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+ values.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId);
+ values.put(EabProvider.ContactColumns.DATA_ID, dataId);
+ values.put(EabProvider.ContactColumns.PHONE_NUMBER, phoneNumber);
+ mContext.getContentResolver().insert(EabProvider.CONTACT_URI, values);
+ }
+
+ /**
+ * Create a fake contact provider that store ContentValues in hashmap when invoke insert()
+ * and convert to cursor when invoke query()
+ */
+ public static class FakeContactProvider extends ContentProvider {
+ private final HashMap<Uri, List<ContentValues>> mFakeProviderData = new HashMap<>();
+
+ public FakeContactProvider() {
+ }
+
+ public void clearData() {
+ mFakeProviderData.clear();
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return convertContentValuesToCursor(mFakeProviderData.get(uri));
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ List<ContentValues> allDataList =
+ mFakeProviderData.computeIfAbsent(uri, k -> new ArrayList<>());
+ allDataList.add(new ContentValues(values));
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values,
+ @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ private Cursor convertContentValuesToCursor(List<ContentValues> valuesList) {
+ if (valuesList != null) {
+ MatrixCursor result =
+ new MatrixCursor(valuesList.get(0).keySet().toArray(new String[0]));
+ for (ContentValues contentValue : valuesList) {
+ MatrixCursor.RowBuilder builder = result.newRow();
+ for (String key : contentValue.keySet()) {
+ builder.add(key, contentValue.get(key));
+ }
+ }
+ return result;
+ } else {
+ return new MatrixCursor(new String[0]);
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
index ceae905..7e3783b 100644
--- a/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
@@ -25,6 +25,7 @@
import android.content.ContentValues;
import android.net.Uri;
+import android.os.Looper;
import android.telephony.ims.RcsContactPresenceTuple;
import android.telephony.ims.RcsContactUceCapability;
import android.test.mock.MockContentResolver;
@@ -69,7 +70,8 @@
mockContentResolver.addProvider(EabProvider.AUTHORITY, mEabProviderTestable);
insertContactInfoToDB();
- mEabController = new EabControllerImpl(mContext, TEST_SUB_ID, null, null);
+ mEabController = new EabControllerImpl(
+ mContext, TEST_SUB_ID, null, Looper.getMainLooper());
}
@After