1/*
2 * Copyright (C) 2012 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.ContentValues;
20import android.content.Context;
21import android.content.Entity;
22import android.net.Uri;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.provider.ContactsContract.Contacts;
26import android.provider.ContactsContract.Data;
27import android.provider.ContactsContract.RawContacts;
28
29import com.android.contacts.common.model.AccountTypeManager;
30import com.android.contacts.common.model.account.AccountType;
31import com.android.contacts.common.model.account.AccountWithDataSet;
32import com.android.contacts.common.model.dataitem.DataItem;
33import com.google.common.base.Objects;
34import com.google.common.collect.Lists;
35
36import java.util.ArrayList;
37import java.util.List;
38
39/**
40 * RawContact represents a single raw contact in the raw contacts database.
41 * It has specialized getters/setters for raw contact
42 * items, and also contains a collection of DataItem objects.  A RawContact contains the information
43 * from a single account.
44 *
45 * This allows RawContact objects to be thought of as a class with raw contact
46 * fields (like account type, name, data set, sync state, etc.) and a list of
47 * DataItem objects that represent contact information elements (like phone
48 * numbers, email, address, etc.).
49 */
50final public class RawContact implements Parcelable {
51
52    private AccountTypeManager mAccountTypeManager;
53    private final ContentValues mValues;
54    private final ArrayList<NamedDataItem> mDataItems;
55
56    final public static class NamedDataItem implements Parcelable {
57        public final Uri mUri;
58
59        // This use to be a DataItem. DataItem creation is now delayed until the point of request
60        // since there is no benefit to storing them here due to the multiple inheritance.
61        // Eventually instanceof still has to be used anyways to determine which sub-class of
62        // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
63        // parcelable.
64        //
65        // Instead of having a common DataItem super class, we should refactor this to be a generic
66        // Object where the object is a concrete class that no longer relies on ContentValues.
67        // (this will also make the classes easier to use).
68        // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
69        // worse than having a DataItem.
70        public final ContentValues mContentValues;
71
72        public NamedDataItem(Uri uri, ContentValues values) {
73            this.mUri = uri;
74            this.mContentValues = values;
75        }
76
77        public NamedDataItem(Parcel parcel) {
78            this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
79            this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
80        }
81
82        @Override
83        public int describeContents() {
84            return 0;
85        }
86
87        @Override
88        public void writeToParcel(Parcel parcel, int i) {
89            parcel.writeParcelable(mUri, i);
90            parcel.writeParcelable(mContentValues, i);
91        }
92
93        public static final Parcelable.Creator<NamedDataItem> CREATOR
94                = new Parcelable.Creator<NamedDataItem>() {
95
96            @Override
97            public NamedDataItem createFromParcel(Parcel parcel) {
98                return new NamedDataItem(parcel);
99            }
100
101            @Override
102            public NamedDataItem[] newArray(int i) {
103                return new NamedDataItem[i];
104            }
105        };
106
107        @Override
108        public int hashCode() {
109            return Objects.hashCode(mUri, mContentValues);
110        }
111
112        @Override
113        public boolean equals(Object obj) {
114            if (obj == null) return false;
115            if (getClass() != obj.getClass()) return false;
116
117            final NamedDataItem other = (NamedDataItem) obj;
118            return Objects.equal(mUri, other.mUri) &&
119                    Objects.equal(mContentValues, other.mContentValues);
120        }
121    }
122
123    public static RawContact createFrom(Entity entity) {
124        final ContentValues values = entity.getEntityValues();
125        final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
126
127        RawContact rawContact = new RawContact(values);
128        for (Entity.NamedContentValues subValue : subValues) {
129            rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
130        }
131        return rawContact;
132    }
133
134    /**
135     * A RawContact object can be created with or without a context.
136     */
137    public RawContact() {
138        this(new ContentValues());
139    }
140
141    public RawContact(ContentValues values) {
142        mValues = values;
143        mDataItems = new ArrayList<NamedDataItem>();
144    }
145
146    /**
147     * Constructor for the parcelable.
148     *
149     * @param parcel The parcel to de-serialize from.
150     */
151    private RawContact(Parcel parcel) {
152        mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
153        mDataItems = Lists.newArrayList();
154        parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
155    }
156
157    @Override
158    public int describeContents() {
159        return 0;
160    }
161
162    @Override
163    public void writeToParcel(Parcel parcel, int i) {
164        parcel.writeParcelable(mValues, i);
165        parcel.writeTypedList(mDataItems);
166    }
167
168    /**
169     * Create for building the parcelable.
170     */
171    public static final Parcelable.Creator<RawContact> CREATOR
172            = new Parcelable.Creator<RawContact>() {
173
174        @Override
175        public RawContact createFromParcel(Parcel parcel) {
176            return new RawContact(parcel);
177        }
178
179        @Override
180        public RawContact[] newArray(int i) {
181            return new RawContact[i];
182        }
183    };
184
185    public AccountTypeManager getAccountTypeManager(Context context) {
186        if (mAccountTypeManager == null) {
187            mAccountTypeManager = AccountTypeManager.getInstance(context);
188        }
189        return mAccountTypeManager;
190    }
191
192    public ContentValues getValues() {
193        return mValues;
194    }
195
196    /**
197     * Returns the id of the raw contact.
198     */
199    public Long getId() {
200        return getValues().getAsLong(RawContacts._ID);
201    }
202
203    /**
204     * Returns the account name of the raw contact.
205     */
206    public String getAccountName() {
207        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
208    }
209
210    /**
211     * Returns the account type of the raw contact.
212     */
213    public String getAccountTypeString() {
214        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
215    }
216
217    /**
218     * Returns the data set of the raw contact.
219     */
220    public String getDataSet() {
221        return getValues().getAsString(RawContacts.DATA_SET);
222    }
223
224    public boolean isDirty() {
225        return getValues().getAsBoolean(RawContacts.DIRTY);
226    }
227
228    public String getSourceId() {
229        return getValues().getAsString(RawContacts.SOURCE_ID);
230    }
231
232    public String getSync1() {
233        return getValues().getAsString(RawContacts.SYNC1);
234    }
235
236    public String getSync2() {
237        return getValues().getAsString(RawContacts.SYNC2);
238    }
239
240    public String getSync3() {
241        return getValues().getAsString(RawContacts.SYNC3);
242    }
243
244    public String getSync4() {
245        return getValues().getAsString(RawContacts.SYNC4);
246    }
247
248    public boolean isDeleted() {
249        return getValues().getAsBoolean(RawContacts.DELETED);
250    }
251
252    public long getContactId() {
253        return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
254    }
255
256    public boolean isStarred() {
257        return getValues().getAsBoolean(Contacts.STARRED);
258    }
259
260    public AccountType getAccountType(Context context) {
261        return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
262    }
263
264    /**
265     * Sets the account name, account type, and data set strings.
266     * Valid combinations for account-name, account-type, data-set
267     * 1) null, null, null (local account)
268     * 2) non-null, non-null, null (valid account without data-set)
269     * 3) non-null, non-null, non-null (valid account with data-set)
270     */
271    private void setAccount(String accountName, String accountType, String dataSet) {
272        final ContentValues values = getValues();
273        if (accountName == null) {
274            if (accountType == null && dataSet == null) {
275                // This is a local account
276                values.putNull(RawContacts.ACCOUNT_NAME);
277                values.putNull(RawContacts.ACCOUNT_TYPE);
278                values.putNull(RawContacts.DATA_SET);
279                return;
280            }
281        } else {
282            if (accountType != null) {
283                // This is a valid account, either with or without a dataSet.
284                values.put(RawContacts.ACCOUNT_NAME, accountName);
285                values.put(RawContacts.ACCOUNT_TYPE, accountType);
286                if (dataSet == null) {
287                    values.putNull(RawContacts.DATA_SET);
288                } else {
289                    values.put(RawContacts.DATA_SET, dataSet);
290                }
291                return;
292            }
293        }
294        throw new IllegalArgumentException(
295                "Not a valid combination of account name, type, and data set.");
296    }
297
298    public void setAccount(AccountWithDataSet accountWithDataSet) {
299        setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet);
300    }
301
302    public void setAccountToLocal() {
303        setAccount(null, null, null);
304    }
305
306    /**
307     * Creates and inserts a DataItem object that wraps the content values, and returns it.
308     */
309    public void addDataItemValues(ContentValues values) {
310        addNamedDataItemValues(Data.CONTENT_URI, values);
311    }
312
313    public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
314        final NamedDataItem namedItem = new NamedDataItem(uri, values);
315        mDataItems.add(namedItem);
316        return namedItem;
317    }
318
319    public ArrayList<ContentValues> getContentValues() {
320        final ArrayList<ContentValues> list = Lists.newArrayListWithCapacity(mDataItems.size());
321        for (NamedDataItem dataItem : mDataItems) {
322            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
323                list.add(dataItem.mContentValues);
324            }
325        }
326        return list;
327    }
328
329    public List<DataItem> getDataItems() {
330        final ArrayList<DataItem> list = Lists.newArrayListWithCapacity(mDataItems.size());
331        for (NamedDataItem dataItem : mDataItems) {
332            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
333                list.add(DataItem.createFrom(dataItem.mContentValues));
334            }
335        }
336        return list;
337    }
338
339    public String toString() {
340        final StringBuilder sb = new StringBuilder();
341        sb.append("RawContact: ").append(mValues);
342        for (RawContact.NamedDataItem namedDataItem : mDataItems) {
343            sb.append("\n  ").append(namedDataItem.mUri);
344            sb.append("\n  -> ").append(namedDataItem.mContentValues);
345        }
346        return sb.toString();
347    }
348
349    @Override
350    public int hashCode() {
351        return Objects.hashCode(mValues, mDataItems);
352    }
353
354    @Override
355    public boolean equals(Object obj) {
356        if (obj == null) return false;
357        if (getClass() != obj.getClass()) return false;
358
359        RawContact other = (RawContact) obj;
360        return Objects.equal(mValues, other.mValues) &&
361                Objects.equal(mDataItems, other.mDataItems);
362    }
363}
364