blob: e9bb9ec64d958524b8dce1e25e0ec1a05577bd41 [file] [log] [blame]
/*
* 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);
}
}