• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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