1package com.android.contacts.editor;
2
3import android.content.AsyncTaskLoader;
4import android.content.ContentResolver;
5import android.content.Context;
6import android.database.Cursor;
7import android.net.Uri;
8import android.os.Parcel;
9import android.os.Parcelable;
10import android.provider.ContactsContract;
11import android.provider.ContactsContract.Contacts;
12import android.provider.ContactsContract.Data;
13import android.provider.ContactsContract.Profile;
14import android.provider.ContactsContract.RawContacts;
15
16import com.android.contacts.model.AccountTypeManager;
17import com.android.contacts.model.account.AccountType;
18
19import java.util.ArrayList;
20import java.util.HashMap;
21import java.util.Map;
22
23/**
24 * Loader for the pick a raw contact to edit activity. Loads all raw contact metadata for the
25 * given Contact {@link Uri}.
26 */
27public class PickRawContactLoader extends
28        AsyncTaskLoader<PickRawContactLoader.RawContactsMetadata> {
29    private Uri mContactUri;
30    private RawContactsMetadata mCachedResult;
31
32    private static final String[] RAW_CONTACT_PROJECTION = new String[] {
33            RawContacts.ACCOUNT_NAME,
34            RawContacts.ACCOUNT_TYPE,
35            RawContacts.DATA_SET,
36            RawContacts._ID,
37            RawContacts.DISPLAY_NAME_PRIMARY,
38            RawContacts.DISPLAY_NAME_ALTERNATIVE
39    };
40
41    private static final String RAW_CONTACT_SELECTION = RawContacts.CONTACT_ID + "=?";
42
43    private static final int ACCOUNT_NAME = 0;
44    private static final int ACCOUNT_TYPE = 1;
45    private static final int DATA_SET = 2;
46    private static final int RAW_CONTACT_ID = 3;
47    private static final int DISPLAY_NAME_PRIMARY = 4;
48    private static final int DISPLAY_NAME_ALTERNATIVE = 5;
49
50    private static final String PHOTO_SELECTION_PREFIX =
51            ContactsContract.Data.RAW_CONTACT_ID + " IN (";
52    private static final String PHOTO_SELECTION_SUFFIX = ") AND " + ContactsContract.Data.MIMETYPE
53            + "=\"" + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "\"";
54
55    public PickRawContactLoader(Context context, Uri contactUri) {
56        super(context);
57        mContactUri = ensureIsContactUri(contactUri);
58    }
59
60    @Override
61    public RawContactsMetadata loadInBackground() {
62        final ContentResolver resolver = getContext().getContentResolver();
63        // Get the id of the contact we're looking at.
64        final Cursor contactCursor = resolver.query(
65                mContactUri, new String[] {Contacts._ID, Contacts.IS_USER_PROFILE}, null,
66                null, null);
67
68        if (contactCursor == null) {
69            return null;
70        }
71
72        if (contactCursor.getCount() < 1) {
73            contactCursor.close();
74            return null;
75        }
76
77        final RawContactsMetadata result = new RawContactsMetadata();
78        try {
79            contactCursor.moveToFirst();
80            result.contactId = contactCursor.getLong(/* Contacts._ID */ 0);
81            result.isUserProfile = contactCursor.getInt(/* Contacts.IS_USER_PROFILE */ 1) == 1;
82        } finally {
83            contactCursor.close();
84        }
85
86        // Load RawContact data
87        final Uri rawContactUri;
88        if (result.isUserProfile) {
89            rawContactUri = ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI;
90        } else {
91            rawContactUri = RawContacts.CONTENT_URI;
92        }
93
94        final Cursor rawContactCursor = resolver.query(
95                rawContactUri, RAW_CONTACT_PROJECTION, RAW_CONTACT_SELECTION,
96                new String[] {Long.toString(result.contactId)}, null);
97
98        if (rawContactCursor == null) {
99            return null;
100        }
101
102        if (rawContactCursor.getCount() < 1) {
103            rawContactCursor.close();
104            return null;
105        }
106
107        rawContactCursor.moveToPosition(-1);
108        final StringBuilder photoSelection = new StringBuilder(PHOTO_SELECTION_PREFIX);
109        final Map<Long, RawContact> rawContactMap = new HashMap<>();
110        try {
111            while (rawContactCursor.moveToNext()) {
112                RawContact rawContact = new RawContact();
113                rawContact.id = rawContactCursor.getLong(RAW_CONTACT_ID);
114                photoSelection.append(rawContact.id).append(',');
115                rawContact.displayName = rawContactCursor.getString(DISPLAY_NAME_PRIMARY);
116                rawContact.displayNameAlt = rawContactCursor.getString(DISPLAY_NAME_ALTERNATIVE);
117                rawContact.accountName = rawContactCursor.getString(ACCOUNT_NAME);
118                rawContact.accountType = rawContactCursor.getString(ACCOUNT_TYPE);
119                rawContact.accountDataSet = rawContactCursor.getString(DATA_SET);
120                result.rawContacts.add(rawContact);
121                rawContactMap.put(rawContact.id, rawContact);
122            }
123        } finally {
124            rawContactCursor.close();
125        }
126
127        // Remove the last ','
128        if (photoSelection.length() > 0) {
129            photoSelection.deleteCharAt(photoSelection.length() - 1);
130        }
131        photoSelection.append(PHOTO_SELECTION_SUFFIX);
132
133        final Uri dataUri = result.isUserProfile
134                ? Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_URI.getPath())
135                : Data.CONTENT_URI;
136        final Cursor photoCursor = resolver.query(
137                dataUri,
138                new String[] {Data.RAW_CONTACT_ID, Contacts.Photo._ID},
139                photoSelection.toString(), null, null);
140
141        if (photoCursor != null) {
142            try {
143                photoCursor.moveToPosition(-1);
144                while (photoCursor.moveToNext()) {
145                    final long rawContactId = photoCursor.getLong(/* Data.RAW_CONTACT_ID */ 0);
146                    rawContactMap.get(rawContactId).photoId =
147                            photoCursor.getLong(/* PHOTO._ID */ 1);
148                }
149            } finally {
150                photoCursor.close();
151            }
152        }
153        return result;
154    }
155
156    @Override
157    public void deliverResult(RawContactsMetadata data) {
158        mCachedResult = data;
159        if (isStarted()) {
160            super.deliverResult(data);
161        }
162    }
163
164    @Override
165    protected void onStartLoading() {
166        super.onStartLoading();
167        if (mCachedResult == null) {
168            forceLoad();
169        } else {
170            deliverResult(mCachedResult);
171        }
172    }
173
174    /**
175     * Ensures that this is a valid contact URI. If invalid, then an exception is
176     * thrown. Otherwise, the original URI is returned.
177     */
178    private static Uri ensureIsContactUri(final Uri uri) {
179        if (uri == null) {
180            throw new IllegalArgumentException("Uri must not be null");
181        }
182        if (!uri.toString().startsWith(Contacts.CONTENT_URI.toString())) {
183            throw new IllegalArgumentException("Invalid contact Uri: " + uri);
184        }
185        return uri;
186    }
187
188    public static class RawContactsMetadata implements Parcelable {
189        public static final Parcelable.Creator<RawContactsMetadata> CREATOR =
190                new Parcelable.Creator<RawContactsMetadata>() {
191                    @Override
192                    public RawContactsMetadata createFromParcel(Parcel source) {
193                        return new RawContactsMetadata(source);
194                    }
195
196                    @Override
197                    public RawContactsMetadata[] newArray(int size) {
198                        return new RawContactsMetadata[size];
199                    }
200                };
201
202        public long contactId;
203        public boolean isUserProfile;
204        public boolean showReadOnly = false;
205        public ArrayList<RawContact> rawContacts = new ArrayList<>();
206
207        public RawContactsMetadata() {}
208
209        private RawContactsMetadata(Parcel in) {
210            contactId = in.readLong();
211            isUserProfile = in.readInt() == 1;
212            showReadOnly = in.readInt() == 1;
213            in.readTypedList(rawContacts, RawContact.CREATOR);
214        }
215
216        /**
217         * Removes all read-only raw contacts.
218         */
219        public void trimReadOnly(AccountTypeManager accountManager) {
220            for (int i = rawContacts.size() - 1; i >= 0 ; i--) {
221                final RawContact rawContact = rawContacts.get(i);
222                final AccountType account = accountManager.getAccountType(
223                        rawContact.accountType, rawContact.accountDataSet);
224                if (!account.areContactsWritable()) {
225                    rawContacts.remove(i);
226                }
227            }
228        }
229
230        /**
231         * Returns the index of the first writable account in this contact or -1 if none exist.
232         */
233        public int getIndexOfFirstWritableAccount(AccountTypeManager accountManager) {
234            for (int i = 0; i < rawContacts.size(); i++) {
235                final RawContact rawContact = rawContacts.get(i);
236                final AccountType account = accountManager.getAccountType(
237                        rawContact.accountType, rawContact.accountDataSet);
238                if (account.areContactsWritable()) {
239                    return i;
240                }
241            }
242
243            return -1;
244        }
245
246        @Override
247        public int describeContents() {
248            return 0;
249        }
250
251        @Override
252        public void writeToParcel(Parcel dest, int flags) {
253            dest.writeLong(contactId);
254            dest.writeInt(isUserProfile ? 1 : 0);
255            dest.writeInt(showReadOnly ? 1 : 0);
256            dest.writeTypedList(rawContacts);
257        }
258    }
259
260    public static class RawContact implements Parcelable {
261        public static final Parcelable.Creator<RawContact> CREATOR =
262                new Parcelable.Creator<RawContact>() {
263                    @Override
264                    public RawContact createFromParcel(Parcel source) {
265                        return new RawContact(source);
266                    }
267
268                    @Override
269                    public RawContact[] newArray(int size) {
270                        return new RawContact[size];
271                    }
272                };
273
274        public long id;
275        public long photoId;
276        public String displayName;
277        public String displayNameAlt;
278        public String accountName;
279        public String accountType;
280        public String accountDataSet;
281
282        public RawContact() {}
283
284        private RawContact(Parcel in) {
285            id = in.readLong();
286            photoId = in.readLong();
287            displayName = in.readString();
288            displayNameAlt = in.readString();
289            accountName = in.readString();
290            accountType = in.readString();
291            accountDataSet = in.readString();
292        }
293
294        @Override
295        public int describeContents() {
296            return 0;
297        }
298
299        @Override
300        public void writeToParcel(Parcel dest, int flags) {
301            dest.writeLong(id);
302            dest.writeLong(photoId);
303            dest.writeString(displayName);
304            dest.writeString(displayNameAlt);
305            dest.writeString(accountName);
306            dest.writeString(accountType);
307            dest.writeString(accountDataSet);
308        }
309    }
310}
311