1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ims.rcs.uce.eab; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.SharedPreferences; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.preference.PreferenceManager; 25 import android.provider.ContactsContract; 26 import android.telephony.TelephonyManager; 27 import android.util.Log; 28 29 import com.android.i18n.phonenumbers.NumberParseException; 30 import com.android.i18n.phonenumbers.PhoneNumberUtil; 31 import com.android.i18n.phonenumbers.Phonenumber; 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.stream.Collectors; 39 40 /** 41 * Sync the contacts from Contact Provider to EAB Provider 42 */ 43 public class EabContactSyncController { 44 private final String TAG = this.getClass().getSimpleName(); 45 46 private static final int NOT_INIT_LAST_UPDATED_TIME = -1; 47 private static final String LAST_UPDATED_TIME_KEY = "eab_last_updated_time"; 48 49 /** 50 * Sync contact from Contact provider to EAB provider. There are 4 kinds of cases need to be 51 * handled when received the contact db changed: 52 * 53 * 1. Contact deleted 54 * 2. Delete the phone number in the contact 55 * 3. Update the phone number 56 * 4. Add a new contact and add phone number 57 * 58 * @return The contacts that need to refresh 59 */ 60 @VisibleForTesting syncContactToEabProvider(Context context)61 public List<Uri> syncContactToEabProvider(Context context) { 62 Log.d(TAG, "syncContactToEabProvider"); 63 List<Uri> refreshContacts = null; 64 StringBuilder selection = new StringBuilder(); 65 String[] selectionArgs = null; 66 67 // Get the last update timestamp from shared preference. 68 long lastUpdatedTimeStamp = getLastUpdatedTime(context); 69 if (lastUpdatedTimeStamp != -1) { 70 selection.append(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + ">?"); 71 selectionArgs = new String[]{String.valueOf(lastUpdatedTimeStamp)}; 72 } 73 74 // Contact deleted cases (case 1) 75 handleContactDeletedCase(context, lastUpdatedTimeStamp); 76 77 // Query the contacts that have not been synchronized to eab contact table. 78 Cursor updatedContact = context.getContentResolver().query( 79 ContactsContract.Data.CONTENT_URI, 80 null, 81 selection.toString(), 82 selectionArgs, 83 null); 84 85 if (updatedContact != null) { 86 Log.d(TAG, "Contact changed count: " + updatedContact.getCount()); 87 88 if (updatedContact.getCount() == 0) { 89 return new ArrayList<>(); 90 } 91 92 // Delete the EAB phone number that not in contact provider (case 2). Updated phone 93 // number(case 3) also delete in here and re-insert in next step. 94 handlePhoneNumberDeletedCase(context, updatedContact); 95 96 // Insert the phone number that not in EAB provider (case 3 and case 4) 97 refreshContacts = handlePhoneNumberInsertedCase(context, updatedContact); 98 99 // Update the last update time in shared preference 100 if (updatedContact.getCount() > 0) { 101 long maxTimestamp = findMaxTimestamp(updatedContact); 102 if (maxTimestamp != Long.MIN_VALUE) { 103 setLastUpdatedTime(context, maxTimestamp); 104 } 105 } 106 updatedContact.close(); 107 } else { 108 Log.e(TAG, "Cursor is null."); 109 } 110 return refreshContacts; 111 } 112 113 /** 114 * Delete the phone numbers that contact has been deleted in contact provider. Query based on 115 * {@link ContactsContract.DeletedContacts#CONTENT_URI} to know which contact has been removed. 116 * 117 * @param timeStamp last updated timestamp 118 */ handleContactDeletedCase(Context context, long timeStamp)119 private void handleContactDeletedCase(Context context, long timeStamp) { 120 String selection = ""; 121 if (timeStamp != -1) { 122 selection = 123 ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">" + timeStamp; 124 } 125 126 Cursor cursor = context.getContentResolver().query( 127 ContactsContract.DeletedContacts.CONTENT_URI, 128 new String[]{ContactsContract.DeletedContacts.CONTACT_ID, 129 ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP}, 130 selection, 131 null, 132 null); 133 134 if (cursor == null) { 135 Log.d(TAG, "handleContactDeletedCase() cursor is null."); 136 return; 137 } 138 139 Log.d(TAG, "(Case 1) The count of contact that need to be deleted: " 140 + cursor.getCount()); 141 142 StringBuilder deleteClause = new StringBuilder(); 143 while (cursor.moveToNext()) { 144 if (deleteClause.length() > 0) { 145 deleteClause.append(" OR "); 146 } 147 148 String contactId = cursor.getString(cursor.getColumnIndex( 149 ContactsContract.DeletedContacts.CONTACT_ID)); 150 deleteClause.append(EabProvider.ContactColumns.CONTACT_ID + "=" + contactId); 151 } 152 153 if (deleteClause.toString().length() > 0) { 154 int number = context.getContentResolver().delete( 155 EabProvider.CONTACT_URI, 156 deleteClause.toString(), 157 null); 158 Log.d(TAG, "(Case 1) Deleted contact count=" + number); 159 } 160 } 161 162 /** 163 * Delete phone numbers that have been deleted in the contact provider. There is no API to get 164 * deleted phone numbers easily, so check all updated contact's phone number and delete the 165 * phone number. It will also delete the phone number that has been changed. 166 */ handlePhoneNumberDeletedCase(Context context, Cursor cursor)167 private void handlePhoneNumberDeletedCase(Context context, Cursor cursor) { 168 // The map represent which contacts have which numbers. 169 Map<String, List<String>> phoneNumberMap = new HashMap<>(); 170 cursor.moveToPosition(-1); 171 while (cursor.moveToNext()) { 172 String mimeType = cursor.getString( 173 cursor.getColumnIndex(ContactsContract.Data.MIMETYPE)); 174 if (!mimeType.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) { 175 continue; 176 } 177 178 String rawContactId = cursor.getString( 179 cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID)); 180 String number = formatNumber(context, cursor.getString( 181 cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))); 182 183 if (phoneNumberMap.containsKey(rawContactId)) { 184 phoneNumberMap.get(rawContactId).add(number); 185 } else { 186 List<String> phoneNumberList = new ArrayList<>(); 187 phoneNumberList.add(number); 188 phoneNumberMap.put(rawContactId, phoneNumberList); 189 } 190 } 191 192 // Build a SQL statement that delete the phone number not exist in contact provider. 193 // For example: 194 // raw_contact_id = 1 AND phone_number NOT IN (12345, 23456) 195 StringBuilder deleteClause = new StringBuilder(); 196 List<String> deleteClauseArgs = new ArrayList<>(); 197 for (Map.Entry<String, List<String>> entry : phoneNumberMap.entrySet()) { 198 String rawContactId = entry.getKey(); 199 List<String> phoneNumberList = entry.getValue(); 200 201 if (deleteClause.length() > 0) { 202 deleteClause.append(" OR "); 203 } 204 205 deleteClause.append("(" + EabProvider.ContactColumns.RAW_CONTACT_ID + "=? "); 206 deleteClauseArgs.add(rawContactId); 207 208 if (phoneNumberList.size() > 0) { 209 String argsList = phoneNumberList.stream() 210 .map(s -> "?") 211 .collect(Collectors.joining(", ")); 212 deleteClause.append(" AND " 213 + EabProvider.ContactColumns.PHONE_NUMBER 214 + " NOT IN (" + argsList + "))"); 215 deleteClauseArgs.addAll(phoneNumberList); 216 } else { 217 deleteClause.append(")"); 218 } 219 } 220 221 int number = context.getContentResolver().delete( 222 EabProvider.CONTACT_URI, 223 deleteClause.toString(), 224 deleteClauseArgs.toArray(new String[0])); 225 Log.d(TAG, "(Case 2, 3) handlePhoneNumberDeletedCase number count= " + number); 226 } 227 228 /** 229 * Insert new phone number. 230 * 231 * @param contactCursor the result of updated contact 232 * @return the contacts that need to refresh 233 */ handlePhoneNumberInsertedCase(Context context, Cursor contactCursor)234 private List<Uri> handlePhoneNumberInsertedCase(Context context, 235 Cursor contactCursor) { 236 List<Uri> refreshContacts = new ArrayList<>(); 237 List<ContentValues> allContactData = new ArrayList<>(); 238 contactCursor.moveToPosition(-1); 239 240 // Query all of contacts that store in eab provider 241 Cursor eabContact = context.getContentResolver().query( 242 EabProvider.CONTACT_URI, 243 null, 244 EabProvider.ContactColumns.DATA_ID + " IS NOT NULL", 245 null, 246 EabProvider.ContactColumns.DATA_ID); 247 248 while (contactCursor.moveToNext()) { 249 String contactId = contactCursor.getString(contactCursor.getColumnIndex( 250 ContactsContract.Data.CONTACT_ID)); 251 String rawContactId = contactCursor.getString(contactCursor.getColumnIndex( 252 ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID)); 253 String dataId = contactCursor.getString( 254 contactCursor.getColumnIndex(ContactsContract.Data._ID)); 255 String mimeType = contactCursor.getString( 256 contactCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)); 257 258 if (!mimeType.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) { 259 continue; 260 } 261 262 String number = formatNumber(context, contactCursor.getString( 263 contactCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))); 264 265 int index = searchDataIdIndex(eabContact, Integer.parseInt(dataId)); 266 if (index == -1) { 267 Log.d(TAG, "Data id does not exist. Insert phone number into EAB db."); 268 refreshContacts.add(Uri.parse(number)); 269 ContentValues data = new ContentValues(); 270 data.put(EabProvider.ContactColumns.CONTACT_ID, contactId); 271 data.put(EabProvider.ContactColumns.DATA_ID, dataId); 272 data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId); 273 data.put(EabProvider.ContactColumns.PHONE_NUMBER, number); 274 allContactData.add(data); 275 } 276 } 277 278 // Insert contacts at once 279 int result = context.getContentResolver().bulkInsert( 280 EabProvider.CONTACT_URI, 281 allContactData.toArray(new ContentValues[0])); 282 Log.d(TAG, "(Case 3, 4) Phone number insert count: " + result); 283 return refreshContacts; 284 } 285 286 /** 287 * Binary search the target data_id in the cursor. 288 * 289 * @param cursor EabProvider contact which sorted by 290 * {@link EabProvider.ContactColumns#DATA_ID} 291 * @param targetDataId the data_id to search for 292 * @return the index of cursor 293 */ searchDataIdIndex(Cursor cursor, int targetDataId)294 private int searchDataIdIndex(Cursor cursor, int targetDataId) { 295 int start = 0; 296 int end = cursor.getCount() - 1; 297 298 while (start <= end) { 299 int position = (start + end) >>> 1; 300 cursor.moveToPosition(position); 301 int dataId = cursor.getInt(cursor.getColumnIndex(EabProvider.ContactColumns.DATA_ID)); 302 303 if (dataId > targetDataId) { 304 end = position - 1; 305 } else if (dataId < targetDataId) { 306 start = position + 1; 307 } else { 308 return position; 309 } 310 } 311 return -1; 312 } 313 314 findMaxTimestamp(Cursor cursor)315 private long findMaxTimestamp(Cursor cursor) { 316 long maxTimestamp = Long.MIN_VALUE; 317 cursor.moveToPosition(-1); 318 while(cursor.moveToNext()) { 319 long lastUpdatedTimeStamp = cursor.getLong(cursor.getColumnIndex( 320 ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP)); 321 Log.d(TAG, lastUpdatedTimeStamp + " " + maxTimestamp); 322 if (lastUpdatedTimeStamp > maxTimestamp) { 323 maxTimestamp = lastUpdatedTimeStamp; 324 } 325 } 326 return maxTimestamp; 327 } 328 setLastUpdatedTime(Context context, long timestamp)329 private void setLastUpdatedTime(Context context, long timestamp) { 330 Log.d(TAG, "setLastUpdatedTime: " + timestamp); 331 SharedPreferences sharedPreferences = 332 PreferenceManager.getDefaultSharedPreferences(context); 333 sharedPreferences.edit().putLong(LAST_UPDATED_TIME_KEY, timestamp).apply(); 334 } 335 getLastUpdatedTime(Context context)336 private long getLastUpdatedTime(Context context) { 337 SharedPreferences sharedPreferences = 338 PreferenceManager.getDefaultSharedPreferences(context); 339 return sharedPreferences.getLong(LAST_UPDATED_TIME_KEY, NOT_INIT_LAST_UPDATED_TIME); 340 } 341 formatNumber(Context context, String number)342 private String formatNumber(Context context, String number) { 343 TelephonyManager manager = context.getSystemService(TelephonyManager.class); 344 String simCountryIso = manager.getSimCountryIso(); 345 if (simCountryIso != null) { 346 simCountryIso = simCountryIso.toUpperCase(); 347 PhoneNumberUtil util = PhoneNumberUtil.getInstance(); 348 try { 349 Phonenumber.PhoneNumber phoneNumber = util.parse(number, simCountryIso); 350 return util.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164); 351 } catch (NumberParseException e) { 352 Log.w(TAG, "formatNumber: could not format " + number + ", error: " + e); 353 } 354 } 355 return number; 356 } 357 } 358