1 /* 2 * Copyright (C) 2024 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.providers.contacts; 18 19 import static com.android.providers.contacts.flags.Flags.disableCp2AccountMoveFlag; 20 import static com.android.providers.contacts.flags.Flags.cp2AccountMoveSyncStubFlag; 21 import static com.android.providers.contacts.flags.Flags.cp2AccountMoveDeleteNonCommonDataRowsFlag; 22 import static com.android.providers.contacts.flags.Flags.disableMoveToIneligibleDefaultAccountFlag; 23 24 import android.accounts.Account; 25 import android.content.ContentUris; 26 import android.content.ContentValues; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.net.Uri; 29 import android.provider.ContactsContract.CommonDataKinds; 30 import android.provider.ContactsContract.Data; 31 import android.provider.ContactsContract.Groups; 32 import android.provider.ContactsContract.RawContacts; 33 import android.provider.ContactsContract.RawContacts.DefaultAccount.DefaultAccountAndState; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.util.Pair; 37 38 import com.android.providers.contacts.util.NeededForTesting; 39 40 import java.util.HashMap; 41 import java.util.Map; 42 import java.util.Set; 43 import java.util.stream.Collectors; 44 45 /** 46 * A class to move {@link RawContacts} and {@link Groups} from one account to another. 47 */ 48 @NeededForTesting 49 public class ContactMover { 50 private static final String TAG = "ContactMover"; 51 private final ContactsDatabaseHelper mDbHelper; 52 private final ContactsProvider2 mCp2; 53 private final DefaultAccountManager mDefaultAccountManager; 54 55 @NeededForTesting ContactMover(ContactsProvider2 contactsProvider, ContactsDatabaseHelper contactsDatabaseHelper, DefaultAccountManager defaultAccountManager)56 public ContactMover(ContactsProvider2 contactsProvider, 57 ContactsDatabaseHelper contactsDatabaseHelper, 58 DefaultAccountManager defaultAccountManager) { 59 mCp2 = contactsProvider; 60 mDbHelper = contactsDatabaseHelper; 61 mDefaultAccountManager = defaultAccountManager; 62 } 63 updateRawContactsAccount( AccountWithDataSet destAccount, Set<Long> rawContactIds)64 private void updateRawContactsAccount( 65 AccountWithDataSet destAccount, Set<Long> rawContactIds) { 66 if (rawContactIds.isEmpty()) { 67 return; 68 } 69 ContentValues values = new ContentValues(); 70 values.put(RawContacts.ACCOUNT_NAME, destAccount.getAccountName()); 71 values.put(RawContacts.ACCOUNT_TYPE, destAccount.getAccountType()); 72 values.put(RawContacts.DATA_SET, destAccount.getDataSet()); 73 values.putNull(RawContacts.SOURCE_ID); 74 values.putNull(RawContacts.SYNC1); 75 values.putNull(RawContacts.SYNC2); 76 values.putNull(RawContacts.SYNC3); 77 values.putNull(RawContacts.SYNC4); 78 79 // actually update the account columns and break the source ID 80 mCp2.update( 81 RawContacts.CONTENT_URI, 82 values, 83 RawContacts._ID + " IN (" + TextUtils.join(",", rawContactIds) + ")", 84 new String[]{}); 85 } 86 updateGroupAccount( AccountWithDataSet destAccount, Set<Long> groupIds)87 private void updateGroupAccount( 88 AccountWithDataSet destAccount, Set<Long> groupIds) { 89 if (groupIds.isEmpty()) { 90 return; 91 } 92 ContentValues values = new ContentValues(); 93 values.put(Groups.ACCOUNT_NAME, destAccount.getAccountName()); 94 values.put(Groups.ACCOUNT_TYPE, destAccount.getAccountType()); 95 values.put(Groups.DATA_SET, destAccount.getDataSet()); 96 values.putNull(Groups.SOURCE_ID); 97 values.putNull(Groups.SYNC1); 98 values.putNull(Groups.SYNC2); 99 values.putNull(Groups.SYNC3); 100 values.putNull(Groups.SYNC4); 101 102 // actually update the account columns and break the source ID 103 mCp2.update( 104 Groups.CONTENT_URI, 105 values, 106 Groups._ID + " IN (" + TextUtils.join(",", groupIds) + ")", 107 new String[]{}); 108 } 109 updateGroupDataRows(Map<Long, Long> groupIdMap)110 private void updateGroupDataRows(Map<Long, Long> groupIdMap) { 111 // for each group in the groupIdMap, update all Group Membership data rows from key to value 112 for (Map.Entry<Long, Long> groupIds : groupIdMap.entrySet()) { 113 mDbHelper.updateGroupMemberships(groupIds.getKey(), groupIds.getValue()); 114 } 115 116 } 117 isAccountTypeMatch( AccountWithDataSet sourceAccount, AccountWithDataSet destAccount)118 private boolean isAccountTypeMatch( 119 AccountWithDataSet sourceAccount, AccountWithDataSet destAccount) { 120 if (sourceAccount.getAccountType() == null) { 121 return destAccount.getAccountType() == null; 122 } 123 124 return sourceAccount.getAccountType().equals(destAccount.getAccountType()); 125 } 126 isDataSetMatch( AccountWithDataSet sourceAccount, AccountWithDataSet destAccount)127 private boolean isDataSetMatch( 128 AccountWithDataSet sourceAccount, AccountWithDataSet destAccount) { 129 if (sourceAccount.getDataSet() == null) { 130 return destAccount.getDataSet() == null; 131 } 132 133 return sourceAccount.getDataSet().equals(destAccount.getDataSet()); 134 } 135 moveNonSystemGroups(AccountWithDataSet sourceAccount, AccountWithDataSet destAccount, boolean insertSyncStubs)136 private void moveNonSystemGroups(AccountWithDataSet sourceAccount, 137 AccountWithDataSet destAccount, boolean insertSyncStubs) { 138 Pair<Set<Long>, Map<Long, Long>> nonSystemGroups = mDbHelper 139 .deDuplicateGroups(sourceAccount, destAccount, /* isSystemGroupQuery= */ false); 140 Set<Long> nonSystemUniqueGroups = nonSystemGroups.first; 141 Map<Long, Long> nonSystemDuplicateGroupMap = nonSystemGroups.second; 142 143 // For non-system groups that are duplicated in source and dest: 144 // 1. update contact data rows (to point do the group in dest) 145 // 2. Set deleted = 1 for dupe groups in source 146 updateGroupDataRows(nonSystemDuplicateGroupMap); 147 for (Map.Entry<Long, Long> groupIds : nonSystemDuplicateGroupMap.entrySet()) { 148 mCp2.deleteGroup(Groups.CONTENT_URI, groupIds.getKey(), false); 149 } 150 151 // For non-system groups that only exist in source: 152 // 1. Write sync stubs for synced groups (if needed) 153 // 2. Update account ids 154 if (!sourceAccount.isLocalAccount() && insertSyncStubs) { 155 mDbHelper.insertGroupSyncStubs(sourceAccount, nonSystemUniqueGroups); 156 } 157 updateGroupAccount(destAccount, nonSystemUniqueGroups); 158 } 159 moveSystemGroups( AccountWithDataSet sourceAccount, AccountWithDataSet destAccount)160 private void moveSystemGroups( 161 AccountWithDataSet sourceAccount, AccountWithDataSet destAccount) { 162 Pair<Set<Long>, Map<Long, Long>> systemGroups = mDbHelper 163 .deDuplicateGroups(sourceAccount, destAccount, /* isSystemGroupQuery= */ true); 164 Set<Long> systemUniqueGroups = systemGroups.first; 165 Map<Long, Long> systemDuplicateGroupMap = systemGroups.second; 166 167 // For system groups in source that have a match in dest: 168 // 1. Update contact data rows (can't delete the existing groups) 169 updateGroupDataRows(systemDuplicateGroupMap); 170 171 // For system groups that only exist in source: 172 // 1. Get content values for the relevant (non-empty) groups 173 // 2. Create a group in destination account (while building an ID map) 174 // 3. Update contact data rows to point at the new group(s) 175 Map<Long, ContentValues> oldIdToNewValues = mDbHelper 176 .getGroupContentValuesForMoveCopy(destAccount, systemUniqueGroups); 177 Map<Long, Long> systemGroupIdMap = new HashMap<>(); 178 for (Map.Entry<Long, ContentValues> idToValues : oldIdToNewValues.entrySet()) { 179 Uri newGroupUri = mCp2.insert(Groups.CONTENT_URI, idToValues.getValue()); 180 if (newGroupUri != null) { 181 Long newGroupId = ContentUris.parseId(newGroupUri); 182 systemGroupIdMap.put(idToValues.getKey(), newGroupId); 183 } 184 } 185 updateGroupDataRows(systemGroupIdMap); 186 187 // now delete membership data rows for any unique groups we skipped - otherwise the contacts 188 // will be left with data rows pointing to the skipped groups in the source account. 189 if (!oldIdToNewValues.isEmpty()) { 190 systemUniqueGroups.removeAll(oldIdToNewValues.keySet()); 191 } 192 mCp2.delete(Data.CONTENT_URI, 193 CommonDataKinds.GroupMembership.GROUP_ROW_ID 194 + " IN (" + TextUtils.join(",", systemUniqueGroups) + ")" 195 + " AND " + Data.MIMETYPE + " = ?", 196 new String[]{CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE} 197 ); 198 } 199 moveGroups(AccountWithDataSet sourceAccount, AccountWithDataSet destAccount, boolean createSyncStubs)200 private void moveGroups(AccountWithDataSet sourceAccount, AccountWithDataSet destAccount, 201 boolean createSyncStubs) { 202 moveNonSystemGroups(sourceAccount, destAccount, createSyncStubs); 203 moveSystemGroups(sourceAccount, destAccount); 204 } 205 getLocalAccounts()206 private Set<AccountWithDataSet> getLocalAccounts() { 207 AccountWithDataSet nullAccount = new AccountWithDataSet( 208 /* accountName= */ null, /* accountType= */ null, /* dataSet= */ null); 209 if (AccountWithDataSet.LOCAL.equals(nullAccount)) { 210 return Set.of(AccountWithDataSet.LOCAL); 211 } 212 return Set.of( 213 AccountWithDataSet.LOCAL, 214 nullAccount); 215 } 216 getSimAccounts()217 private Set<AccountWithDataSet> getSimAccounts() { 218 return mDbHelper.getAllSimAccounts().stream() 219 .map(simAccount -> 220 new AccountWithDataSet( 221 simAccount.getAccountName(), simAccount.getAccountType(), null)) 222 .collect(Collectors.toSet()); 223 } 224 getCloudDefaultAccount()225 Account getCloudDefaultAccount() { 226 DefaultAccountAndState defaultAccount = mDefaultAccountManager.pullDefaultAccount(); 227 if (defaultAccount.getState() != DefaultAccountAndState.DEFAULT_ACCOUNT_STATE_CLOUD) { 228 Log.w(TAG, "No default cloud account set"); 229 return null; 230 } 231 Account account = defaultAccount.getAccount(); 232 assert account != null; 233 if (disableMoveToIneligibleDefaultAccountFlag() 234 && !mDefaultAccountManager.getEligibleCloudAccounts().contains(account)) { 235 Log.w(TAG, "Ineligible default cloud account set"); 236 return null; 237 } 238 239 return account; 240 } 241 242 /** 243 * Moves {@link RawContacts} and {@link Groups} from the local account(s) to the Cloud Default 244 * Account (if any). 245 */ 246 // Keep it in proguard for testing: once it's used in production code, remove this annotation. 247 @NeededForTesting moveLocalToCloudDefaultAccount()248 void moveLocalToCloudDefaultAccount() { 249 if (disableCp2AccountMoveFlag()) { 250 Log.w(TAG, "moveLocalToCloudDefaultAccount: flag disabled"); 251 return; 252 } 253 254 // Check if there is a cloud default account set 255 // - if not, then we don't need to do anything 256 // - if there is, then that's our destAccount, get the AccountWithDataSet 257 Account account = getCloudDefaultAccount(); 258 if (account == null) { 259 Log.w(TAG, 260 "moveLocalToCloudDefaultAccount with no eligible cloud default account set"); 261 return; 262 } 263 264 AccountWithDataSet destAccount = new AccountWithDataSet( 265 account.name, account.type, /* dataSet= */ null); 266 267 // Move any contacts from the local account to the destination account 268 moveRawContacts(getLocalAccounts(), destAccount); 269 } 270 271 /** 272 * Moves {@link RawContacts} and {@link Groups} from the SIM account(s) to the Cloud Default 273 * Account (if any). 274 */ 275 // Keep it in proguard for testing: once it's used in production code, remove this annotation. 276 @NeededForTesting moveSimToCloudDefaultAccount()277 void moveSimToCloudDefaultAccount() { 278 if (disableCp2AccountMoveFlag()) { 279 Log.w(TAG, "moveSimToCloudDefaultAccount: flag disabled"); 280 return; 281 } 282 283 // Check if there is a cloud default account set 284 // - if not, then we don't need to do anything 285 // - if there is, then that's our destAccount, get the AccountWithDataSet 286 Account account = getCloudDefaultAccount(); 287 if (account == null) { 288 Log.w(TAG, "moveSimToCloudDefaultAccount with no eligible cloud default account set"); 289 return; 290 } 291 292 AccountWithDataSet destAccount = new AccountWithDataSet( 293 account.name, account.type, /* dataSet= */ null); 294 295 // Move any contacts from the sim accounts to the destination account 296 moveRawContacts(getSimAccounts(), destAccount); 297 } 298 299 /** 300 * Gets the number of {@link RawContacts} in the local account(s) which may be moved using 301 * {@link ContactMover#moveLocalToCloudDefaultAccount} (if any). 302 * 303 * @return the number of {@link RawContacts} in the local account(s), or 0 if there is no Cloud 304 * Default Account. 305 */ 306 // Keep it in proguard for testing: once it's used in production code, remove this annotation. 307 @NeededForTesting getNumberLocalContacts()308 int getNumberLocalContacts() { 309 if (disableCp2AccountMoveFlag()) { 310 Log.w(TAG, "getNumberLocalContacts: flag disabled"); 311 return 0; 312 } 313 314 // Check if there is a cloud default account set 315 // - if not, then we don't need to do anything, count = 0 316 // - if there is, then do the count 317 Account account = getCloudDefaultAccount(); 318 if (account == null) { 319 Log.w(TAG, "getNumberLocalContacts with no eligible cloud default account set"); 320 return 0; 321 } 322 323 // Count any contacts in the local account(s) 324 return countRawContactsForAccounts(getLocalAccounts()); 325 } 326 327 /** 328 * Gets the number of {@link RawContacts} in the SIM account(s) which may be moved using 329 * {@link ContactMover#moveSimToCloudDefaultAccount} (if any). 330 * 331 * @return the number of {@link RawContacts} in the SIM account(s), or 0 if there is no Cloud 332 * Default Account. 333 */ 334 // Keep it in proguard for testing: once it's used in production code, remove this annotation. 335 @NeededForTesting getNumberSimContacts()336 int getNumberSimContacts() { 337 if (disableCp2AccountMoveFlag()) { 338 Log.w(TAG, "getNumberSimContacts: flag disabled"); 339 return 0; 340 } 341 342 // Check if there is a cloud default account set 343 // - if not, then we don't need to do anything, count = 0 344 // - if there is, then do the count 345 Account account = getCloudDefaultAccount(); 346 if (account == null) { 347 Log.w(TAG, "getNumberSimContacts with no eligible cloud default account set"); 348 return 0; 349 } 350 351 // Count any contacts in the sim accounts. 352 return countRawContactsForAccounts(getSimAccounts()); 353 } 354 355 /** 356 * Moves {@link RawContacts} and {@link Groups} from one account to another. 357 * 358 * @param sourceAccounts the source {@link AccountWithDataSet}s to move contacts and groups 359 * from. 360 * @param destAccount the destination {@link AccountWithDataSet} to move contacts and groups 361 * to. 362 */ 363 // Keep it in proguard for testing: once it's used in production code, remove this annotation. 364 @NeededForTesting moveRawContacts(Set<AccountWithDataSet> sourceAccounts, AccountWithDataSet destAccount)365 void moveRawContacts(Set<AccountWithDataSet> sourceAccounts, AccountWithDataSet destAccount) { 366 if (disableCp2AccountMoveFlag()) { 367 Log.w(TAG, "moveRawContacts: flag disabled"); 368 return; 369 } 370 moveRawContactsForAccounts( 371 sourceAccounts, destAccount, /* insertSyncStubs= */ false); 372 } 373 374 /** 375 * Moves {@link RawContacts} and {@link Groups} from one account to another, while writing sync 376 * stubs in the source account to notify relevant sync adapters in the source account of the 377 * move. 378 * 379 * @param sourceAccounts the source {@link AccountWithDataSet}s to move contacts and groups 380 * from. 381 * @param destAccount the destination {@link AccountWithDataSet} to move contacts and groups 382 * to. 383 */ 384 // Keep it in proguard for testing: once it's used in production code, remove this annotation. 385 @NeededForTesting moveRawContactsWithSyncStubs(Set<AccountWithDataSet> sourceAccounts, AccountWithDataSet destAccount)386 void moveRawContactsWithSyncStubs(Set<AccountWithDataSet> sourceAccounts, 387 AccountWithDataSet destAccount) { 388 if (disableCp2AccountMoveFlag() || !cp2AccountMoveSyncStubFlag()) { 389 Log.w(TAG, "moveRawContactsWithSyncStubs: flags disabled"); 390 return; 391 } 392 moveRawContactsForAccounts(sourceAccounts, destAccount, /* insertSyncStubs= */ true); 393 } 394 countRawContactsForAccounts(Set<AccountWithDataSet> sourceAccounts)395 private int countRawContactsForAccounts(Set<AccountWithDataSet> sourceAccounts) { 396 return mDbHelper.countRawContactsQuery(sourceAccounts); 397 } 398 moveRawContactsForAccounts(Set<AccountWithDataSet> sourceAccounts, AccountWithDataSet destAccount, boolean insertSyncStubs)399 private void moveRawContactsForAccounts(Set<AccountWithDataSet> sourceAccounts, 400 AccountWithDataSet destAccount, boolean insertSyncStubs) { 401 if (sourceAccounts.contains(destAccount)) { 402 throw new IllegalArgumentException("Source and destination accounts must differ"); 403 } 404 405 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 406 db.beginTransaction(); 407 try { 408 for (AccountWithDataSet source : sourceAccounts) { 409 moveRawContactsInternal(source, destAccount, insertSyncStubs); 410 } 411 412 db.setTransactionSuccessful(); 413 } finally { 414 db.endTransaction(); 415 } 416 } 417 moveRawContactsInternal(AccountWithDataSet sourceAccount, AccountWithDataSet destAccount, boolean insertSyncStubs)418 private void moveRawContactsInternal(AccountWithDataSet sourceAccount, 419 AccountWithDataSet destAccount, boolean insertSyncStubs) { 420 // If we are moving between account types or data sets, delete non-portable data rows 421 // from the source 422 if (cp2AccountMoveDeleteNonCommonDataRowsFlag()) { 423 if (!isAccountTypeMatch(sourceAccount, destAccount) 424 || !isDataSetMatch(sourceAccount, destAccount)) { 425 mDbHelper.deleteNonCommonDataRows(sourceAccount); 426 } 427 } 428 429 // Move any groups and group memberships from the source to destination account 430 moveGroups(sourceAccount, destAccount, insertSyncStubs); 431 432 // Next, compare raw contacts from source and destination accounts, find the unique 433 // raw contacts from source account; 434 Pair<Set<Long>, Set<Long>> sourceRawContactIds = 435 mDbHelper.deDuplicateRawContacts(sourceAccount, destAccount); 436 Set<Long> nonDuplicates = sourceRawContactIds.first; 437 Set<Long> duplicates = sourceRawContactIds.second; 438 439 if (!sourceAccount.isLocalAccount() && insertSyncStubs) { 440 /* 441 If the source account isn't a device account, and we want to write stub contacts 442 for the move, create them now. 443 This ensures any sync adapters on the source account won't just sync the moved 444 contacts back down (creating duplicates). 445 */ 446 mDbHelper.insertRawContactSyncStubs(sourceAccount, nonDuplicates); 447 } 448 449 // move the contacts to the destination account 450 updateRawContactsAccount(destAccount, nonDuplicates); 451 452 // Last, clear the duplicates. 453 // Since these are duplicates, we don't need to do anything else with them 454 for (long rawContactId : duplicates) { 455 mCp2.deleteRawContact( 456 rawContactId, 457 mDbHelper.getContactId(rawContactId), 458 false); 459 } 460 } 461 462 } 463