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
17package com.android.contacts.common.model;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentProviderOperation.Builder;
21import android.content.ContentValues;
22import android.content.Context;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.provider.BaseColumns;
27import android.provider.ContactsContract.Data;
28import android.provider.ContactsContract.Profile;
29import android.provider.ContactsContract.RawContacts;
30import android.util.Log;
31
32import com.android.contacts.common.model.AccountTypeManager;
33import com.android.contacts.common.model.ValuesDelta;
34import com.android.contacts.common.model.account.AccountType;
35import com.android.contacts.common.test.NeededForTesting;
36import com.google.common.collect.Lists;
37import com.google.common.collect.Maps;
38
39import java.util.ArrayList;
40import 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 */
55public 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
78    public RawContactDelta() {
79    }
80
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     */
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     */
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
143    public ValuesDelta getValues() {
144        return mValues;
145    }
146
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     */
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     */
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
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     */
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
218    public Long getRawContactId() {
219        return getValues().getAsLong(RawContacts._ID);
220    }
221
222    public String getAccountName() {
223        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
224    }
225
226    public String getAccountType() {
227        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
228    }
229
230    public String getDataSet() {
231        return getValues().getAsString(RawContacts.DATA_SET);
232    }
233
234    public AccountType getAccountType(AccountTypeManager manager) {
235        return manager.getAccountType(getAccountType(), getDataSet());
236    }
237
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     */
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
255    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
256        return getMimeEntries(mimeType, false);
257    }
258
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
272    public boolean hasMimeEntries(String mimeType) {
273        return mEntries.containsKey(mimeType);
274    }
275
276    public ValuesDelta addEntry(ValuesDelta entry) {
277        final String mimeType = entry.getMimetype();
278        getMimeEntries(mimeType, true).add(entry);
279        return entry;
280    }
281
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     */
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     */
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
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
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     */
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
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     */
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     */
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     */
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     */
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} */
507    public int describeContents() {
508        // Nothing special about this parcel
509        return 0;
510    }
511
512    /** {@inheritDoc} */
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
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     */
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