1 /*
2  * Copyright (C) 2009 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.contacts.common.model;
18 
19 import android.content.ContentProviderOperation;
20 import android.content.ContentProviderOperation.Builder;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.net.Uri;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.provider.BaseColumns;
27 import android.provider.ContactsContract.Data;
28 import android.provider.ContactsContract.Profile;
29 import android.provider.ContactsContract.RawContacts;
30 import android.util.Log;
31 
32 import com.android.contacts.common.model.AccountTypeManager;
33 import com.android.contacts.common.model.ValuesDelta;
34 import com.android.contacts.common.model.account.AccountType;
35 import com.android.contacts.common.testing.NeededForTesting;
36 import com.google.common.collect.Lists;
37 import com.google.common.collect.Maps;
38 
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 
42 /**
43  * Contains a {@link RawContact} and records any modifications separately so the
44  * original {@link RawContact} can be swapped out with a newer version and the
45  * changes still cleanly applied.
46  * <p>
47  * One benefit of this approach is that we can build changes entirely on an
48  * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
49  * <p>
50  * When applying modifications over an {@link RawContact}, we try finding the
51  * original {@link Data#_ID} rows where the modifications took place. If those
52  * rows are missing from the new {@link RawContact}, we know the original data must
53  * be deleted, but to preserve the user modifications we treat as an insert.
54  */
55 public class RawContactDelta implements Parcelable {
56     // TODO: optimize by using contentvalues pool, since we allocate so many of them
57 
58     private static final String TAG = "EntityDelta";
59     private static final boolean LOGV = false;
60 
61     /**
62      * Direct values from {@link Entity#getEntityValues()}.
63      */
64     private ValuesDelta mValues;
65 
66     /**
67      * URI used for contacts queries, by default it is set to query raw contacts.
68      * It can be set to query the profile's raw contact(s).
69      */
70     private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
71 
72     /**
73      * Internal map of children values from {@link Entity#getSubValues()}, which
74      * we store here sorted into {@link Data#MIMETYPE} bins.
75      */
76     private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
77 
RawContactDelta()78     public RawContactDelta() {
79     }
80 
RawContactDelta(ValuesDelta values)81     public RawContactDelta(ValuesDelta values) {
82         mValues = values;
83     }
84 
85     /**
86      * Build an {@link RawContactDelta} using the given {@link RawContact} as a
87      * starting point; the "before" snapshot.
88      */
fromBefore(RawContact before)89     public static RawContactDelta fromBefore(RawContact before) {
90         final RawContactDelta rawContactDelta = new RawContactDelta();
91         rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
92         rawContactDelta.mValues.setIdColumn(RawContacts._ID);
93         for (final ContentValues values : before.getContentValues()) {
94             rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
95         }
96         return rawContactDelta;
97     }
98 
99     /**
100      * Merge the "after" values from the given {@link RawContactDelta} onto the
101      * "before" state represented by this {@link RawContactDelta}, discarding any
102      * existing "after" states. This is typically used when re-parenting changes
103      * onto an updated {@link Entity}.
104      */
mergeAfter(RawContactDelta local, RawContactDelta remote)105     public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
106         // Bail early if trying to merge delete with missing local
107         final ValuesDelta remoteValues = remote.mValues;
108         if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
109 
110         // Create local version if none exists yet
111         if (local == null) local = new RawContactDelta();
112 
113         if (LOGV) {
114             final Long localVersion = (local.mValues == null) ? null : local.mValues
115                     .getAsLong(RawContacts.VERSION);
116             final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
117             Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
118                     + localVersion);
119         }
120 
121         // Create values if needed, and merge "after" changes
122         local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
123 
124         // Find matching local entry for each remote values, or create
125         for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
126             for (ValuesDelta remoteEntry : mimeEntries) {
127                 final Long childId = remoteEntry.getId();
128 
129                 // Find or create local match and merge
130                 final ValuesDelta localEntry = local.getEntry(childId);
131                 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
132 
133                 if (localEntry == null && merged != null) {
134                     // No local entry before, so insert
135                     local.addEntry(merged);
136                 }
137             }
138         }
139 
140         return local;
141     }
142 
getValues()143     public ValuesDelta getValues() {
144         return mValues;
145     }
146 
isContactInsert()147     public boolean isContactInsert() {
148         return mValues.isInsert();
149     }
150 
151     /**
152      * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
153      * which may return null when no entry exists.
154      */
getPrimaryEntry(String mimeType)155     public ValuesDelta getPrimaryEntry(String mimeType) {
156         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
157         if (mimeEntries == null) return null;
158 
159         for (ValuesDelta entry : mimeEntries) {
160             if (entry.isPrimary()) {
161                 return entry;
162             }
163         }
164 
165         // When no direct primary, return something
166         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
167     }
168 
169     /**
170      * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
171      * @see #getSuperPrimaryEntry(String, boolean)
172      */
getSuperPrimaryEntry(String mimeType)173     public ValuesDelta getSuperPrimaryEntry(String mimeType) {
174         return getSuperPrimaryEntry(mimeType, true);
175     }
176 
177     /**
178      * Returns the super-primary entry for the given mime type
179      * @param forceSelection if true, will try to return some value even if a super-primary
180      *     doesn't exist (may be a primary, or just a random item
181      * @return
182      */
183     @NeededForTesting
getSuperPrimaryEntry(String mimeType, boolean forceSelection)184     public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
185         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
186         if (mimeEntries == null) return null;
187 
188         ValuesDelta primary = null;
189         for (ValuesDelta entry : mimeEntries) {
190             if (entry.isSuperPrimary()) {
191                 return entry;
192             } else if (entry.isPrimary()) {
193                 primary = entry;
194             }
195         }
196 
197         if (!forceSelection) {
198             return null;
199         }
200 
201         // When no direct super primary, return something
202         if (primary != null) {
203             return primary;
204         }
205         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
206     }
207 
208     /**
209      * Return the AccountType that this raw-contact belongs to.
210      */
getRawContactAccountType(Context context)211     public AccountType getRawContactAccountType(Context context) {
212         ContentValues entityValues = getValues().getCompleteValues();
213         String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
214         String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
215         return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
216     }
217 
getRawContactId()218     public Long getRawContactId() {
219         return getValues().getAsLong(RawContacts._ID);
220     }
221 
getAccountName()222     public String getAccountName() {
223         return getValues().getAsString(RawContacts.ACCOUNT_NAME);
224     }
225 
getAccountType()226     public String getAccountType() {
227         return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
228     }
229 
getDataSet()230     public String getDataSet() {
231         return getValues().getAsString(RawContacts.DATA_SET);
232     }
233 
getAccountType(AccountTypeManager manager)234     public AccountType getAccountType(AccountTypeManager manager) {
235         return manager.getAccountType(getAccountType(), getDataSet());
236     }
237 
isVisible()238     public boolean isVisible() {
239         return getValues().isVisible();
240     }
241 
242     /**
243      * Return the list of child {@link ValuesDelta} from our optimized map,
244      * creating the list if requested.
245      */
getMimeEntries(String mimeType, boolean lazyCreate)246     private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
247         ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
248         if (mimeEntries == null && lazyCreate) {
249             mimeEntries = Lists.newArrayList();
250             mEntries.put(mimeType, mimeEntries);
251         }
252         return mimeEntries;
253     }
254 
getMimeEntries(String mimeType)255     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
256         return getMimeEntries(mimeType, false);
257     }
258 
getMimeEntriesCount(String mimeType, boolean onlyVisible)259     public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
260         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
261         if (mimeEntries == null) return 0;
262 
263         int count = 0;
264         for (ValuesDelta child : mimeEntries) {
265             // Skip deleted items when requesting only visible
266             if (onlyVisible && !child.isVisible()) continue;
267             count++;
268         }
269         return count;
270     }
271 
hasMimeEntries(String mimeType)272     public boolean hasMimeEntries(String mimeType) {
273         return mEntries.containsKey(mimeType);
274     }
275 
addEntry(ValuesDelta entry)276     public ValuesDelta addEntry(ValuesDelta entry) {
277         final String mimeType = entry.getMimetype();
278         getMimeEntries(mimeType, true).add(entry);
279         return entry;
280     }
281 
getContentValues()282     public ArrayList<ContentValues> getContentValues() {
283         ArrayList<ContentValues> values = Lists.newArrayList();
284         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
285             for (ValuesDelta entry : mimeEntries) {
286                 if (!entry.isDelete()) {
287                     values.add(entry.getCompleteValues());
288                 }
289             }
290         }
291         return values;
292     }
293 
294     /**
295      * Find entry with the given {@link BaseColumns#_ID} value.
296      */
getEntry(Long childId)297     public ValuesDelta getEntry(Long childId) {
298         if (childId == null) {
299             // Requesting an "insert" entry, which has no "before"
300             return null;
301         }
302 
303         // Search all children for requested entry
304         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
305             for (ValuesDelta entry : mimeEntries) {
306                 if (childId.equals(entry.getId())) {
307                     return entry;
308                 }
309             }
310         }
311         return null;
312     }
313 
314     /**
315      * Return the total number of {@link ValuesDelta} contained.
316      */
getEntryCount(boolean onlyVisible)317     public int getEntryCount(boolean onlyVisible) {
318         int count = 0;
319         for (String mimeType : mEntries.keySet()) {
320             count += getMimeEntriesCount(mimeType, onlyVisible);
321         }
322         return count;
323     }
324 
325     @Override
equals(Object object)326     public boolean equals(Object object) {
327         if (object instanceof RawContactDelta) {
328             final RawContactDelta other = (RawContactDelta)object;
329 
330             // Equality failed if parent values different
331             if (!other.mValues.equals(mValues)) return false;
332 
333             for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
334                 for (ValuesDelta child : mimeEntries) {
335                     // Equality failed if any children unmatched
336                     if (!other.containsEntry(child)) return false;
337                 }
338             }
339 
340             // Passed all tests, so equal
341             return true;
342         }
343         return false;
344     }
345 
containsEntry(ValuesDelta entry)346     private boolean containsEntry(ValuesDelta entry) {
347         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
348             for (ValuesDelta child : mimeEntries) {
349                 // Contained if we find any child that matches
350                 if (child.equals(entry)) return true;
351             }
352         }
353         return false;
354     }
355 
356     /**
357      * Mark this entire object deleted, including any {@link ValuesDelta}.
358      */
markDeleted()359     public void markDeleted() {
360         this.mValues.markDeleted();
361         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
362             for (ValuesDelta child : mimeEntries) {
363                 child.markDeleted();
364             }
365         }
366     }
367 
368     @Override
toString()369     public String toString() {
370         final StringBuilder builder = new StringBuilder();
371         builder.append("\n(");
372         builder.append("Uri=");
373         builder.append(mContactsQueryUri);
374         builder.append(", Values=");
375         builder.append(mValues != null ? mValues.toString() : "null");
376         builder.append(", Entries={");
377         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
378             for (ValuesDelta child : mimeEntries) {
379                 builder.append("\n\t");
380                 child.toString(builder);
381             }
382         }
383         builder.append("\n})\n");
384         return builder.toString();
385     }
386 
387     /**
388      * Consider building the given {@link ContentProviderOperation.Builder} and
389      * appending it to the given list, which only happens if builder is valid.
390      */
possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder)391     private void possibleAdd(ArrayList<ContentProviderOperation> diff,
392             ContentProviderOperation.Builder builder) {
393         if (builder != null) {
394             diff.add(builder.build());
395         }
396     }
397 
398     /**
399      * Build a list of {@link ContentProviderOperation} that will assert any
400      * "before" state hasn't changed. This is maintained separately so that all
401      * asserts can take place before any updates occur.
402      */
buildAssert(ArrayList<ContentProviderOperation> buildInto)403     public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
404         final boolean isContactInsert = mValues.isInsert();
405         if (!isContactInsert) {
406             // Assert version is consistent while persisting changes
407             final Long beforeId = mValues.getId();
408             final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
409             if (beforeId == null || beforeVersion == null) return;
410 
411             final ContentProviderOperation.Builder builder = ContentProviderOperation
412                     .newAssertQuery(mContactsQueryUri);
413             builder.withSelection(RawContacts._ID + "=" + beforeId, null);
414             builder.withValue(RawContacts.VERSION, beforeVersion);
415             buildInto.add(builder.build());
416         }
417     }
418 
419     /**
420      * Build a list of {@link ContentProviderOperation} that will transform the
421      * current "before" {@link Entity} state into the modified state which this
422      * {@link RawContactDelta} represents.
423      */
buildDiff(ArrayList<ContentProviderOperation> buildInto)424     public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
425         final int firstIndex = buildInto.size();
426 
427         final boolean isContactInsert = mValues.isInsert();
428         final boolean isContactDelete = mValues.isDelete();
429         final boolean isContactUpdate = !isContactInsert && !isContactDelete;
430 
431         final Long beforeId = mValues.getId();
432 
433         Builder builder;
434 
435         if (isContactInsert) {
436             // TODO: for now simply disabling aggregation when a new contact is
437             // created on the phone.  In the future, will show aggregation suggestions
438             // after saving the contact.
439             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
440         }
441 
442         // Build possible operation at Contact level
443         builder = mValues.buildDiff(mContactsQueryUri);
444         possibleAdd(buildInto, builder);
445 
446         // Build operations for all children
447         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
448             for (ValuesDelta child : mimeEntries) {
449                 // Ignore children if parent was deleted
450                 if (isContactDelete) continue;
451 
452                 // Use the profile data URI if the contact is the profile.
453                 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
454                     builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
455                             RawContacts.Data.CONTENT_DIRECTORY));
456                 } else {
457                     builder = child.buildDiff(Data.CONTENT_URI);
458                 }
459 
460                 if (child.isInsert()) {
461                     if (isContactInsert) {
462                         // Parent is brand new insert, so back-reference _id
463                         builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
464                     } else {
465                         // Inserting under existing, so fill with known _id
466                         builder.withValue(Data.RAW_CONTACT_ID, beforeId);
467                     }
468                 } else if (isContactInsert && builder != null) {
469                     // Child must be insert when Contact insert
470                     throw new IllegalArgumentException("When parent insert, child must be also");
471                 }
472                 possibleAdd(buildInto, builder);
473             }
474         }
475 
476         final boolean addedOperations = buildInto.size() > firstIndex;
477         if (addedOperations && isContactUpdate) {
478             // Suspend aggregation while persisting updates
479             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
480             buildInto.add(firstIndex, builder.build());
481 
482             // Restore aggregation mode as last operation
483             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
484             buildInto.add(builder.build());
485         } else if (isContactInsert) {
486             // Restore aggregation mode as last operation
487             builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
488             builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
489             builder.withSelection(RawContacts._ID + "=?", new String[1]);
490             builder.withSelectionBackReference(0, firstIndex);
491             buildInto.add(builder.build());
492         }
493     }
494 
495     /**
496      * Build a {@link ContentProviderOperation} that changes
497      * {@link RawContacts#AGGREGATION_MODE} to the given value.
498      */
buildSetAggregationMode(Long beforeId, int mode)499     protected Builder buildSetAggregationMode(Long beforeId, int mode) {
500         Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
501         builder.withValue(RawContacts.AGGREGATION_MODE, mode);
502         builder.withSelection(RawContacts._ID + "=" + beforeId, null);
503         return builder;
504     }
505 
506     /** {@inheritDoc} */
describeContents()507     public int describeContents() {
508         // Nothing special about this parcel
509         return 0;
510     }
511 
512     /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)513     public void writeToParcel(Parcel dest, int flags) {
514         final int size = this.getEntryCount(false);
515         dest.writeInt(size);
516         dest.writeParcelable(mValues, flags);
517         dest.writeParcelable(mContactsQueryUri, flags);
518         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
519             for (ValuesDelta child : mimeEntries) {
520                 dest.writeParcelable(child, flags);
521             }
522         }
523     }
524 
readFromParcel(Parcel source)525     public void readFromParcel(Parcel source) {
526         final ClassLoader loader = getClass().getClassLoader();
527         final int size = source.readInt();
528         mValues = source.<ValuesDelta> readParcelable(loader);
529         mContactsQueryUri = source.<Uri> readParcelable(loader);
530         for (int i = 0; i < size; i++) {
531             final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
532             this.addEntry(child);
533         }
534     }
535 
536     /**
537      * Used to set the query URI to the profile URI to store profiles.
538      */
setProfileQueryUri()539     public void setProfileQueryUri() {
540         mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
541     }
542 
543     public static final Parcelable.Creator<RawContactDelta> CREATOR =
544             new Parcelable.Creator<RawContactDelta>() {
545         public RawContactDelta createFromParcel(Parcel in) {
546             final RawContactDelta state = new RawContactDelta();
547             state.readFromParcel(in);
548             return state;
549         }
550 
551         public RawContactDelta[] newArray(int size) {
552             return new RawContactDelta[size];
553         }
554     };
555 
556 }
557