1/*
2 * Copyright (C) 2010 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.model;
18
19import android.content.ContentUris;
20import android.net.Uri;
21import android.provider.ContactsContract;
22import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
23import android.provider.ContactsContract.CommonDataKinds.StructuredName;
24import android.provider.ContactsContract.Contacts;
25import android.provider.ContactsContract.Data;
26import android.provider.ContactsContract.DisplayNameSources;
27import android.provider.ContactsContract.RawContacts;
28import android.provider.ContactsContract.StatusUpdates;
29import android.test.LoaderTestCase;
30import android.test.suitebuilder.annotation.LargeTest;
31
32import com.android.contacts.compat.CompatUtils;
33import com.android.contacts.model.account.AccountType;
34import com.android.contacts.model.account.AccountWithDataSet;
35import com.android.contacts.model.account.BaseAccountType;
36import com.android.contacts.test.mocks.ContactsMockContext;
37import com.android.contacts.test.mocks.MockAccountTypeManager;
38import com.android.contacts.test.mocks.MockContentProvider;
39import com.android.contacts.testing.InjectedServices;
40import com.android.contacts.util.Constants;
41
42import com.google.common.collect.Lists;
43
44import org.json.JSONException;
45import org.json.JSONObject;
46
47import java.util.List;
48
49/**
50 * Runs ContactLoader tests for the the contact-detail and editor view.
51 */
52@LargeTest
53public class ContactLoaderTest extends LoaderTestCase {
54    private static final long CONTACT_ID = 1;
55    private static final long RAW_CONTACT_ID = 11;
56    private static final long DATA_ID = 21;
57    private static final String LOOKUP_KEY = "aa%12%@!";
58
59    private ContactsMockContext mMockContext;
60    private MockContentProvider mContactsProvider;
61
62    @Override
63    protected void setUp() throws Exception {
64        super.setUp();
65        mMockContext = new ContactsMockContext(getContext());
66        mContactsProvider = mMockContext.getContactsProvider();
67
68        InjectedServices services = new InjectedServices();
69        AccountType accountType = new BaseAccountType() {
70            @Override
71            public boolean areContactsWritable() {
72                return false;
73            }
74        };
75        accountType.accountType = "mockAccountType";
76
77        AccountWithDataSet account =
78                new AccountWithDataSet("mockAccountName", "mockAccountType", null);
79
80        AccountTypeManager.setInstanceForTest(
81                new MockAccountTypeManager(
82                        new AccountType[]{accountType}, new AccountWithDataSet[]{account}));
83    }
84
85    @Override
86    protected void tearDown() throws Exception {
87        mMockContext = null;
88        mContactsProvider = null;
89        super.tearDown();
90    }
91
92    private Contact assertLoadContact(Uri uri) {
93        final ContactLoader loader = new ContactLoader(mMockContext, uri, true);
94        return getLoaderResultSynchronously(loader);
95    }
96
97    public void testNullUri() {
98        Contact result = assertLoadContact(null);
99        assertTrue(result.isError());
100    }
101
102    public void testEmptyUri() {
103        Contact result = assertLoadContact(Uri.EMPTY);
104        assertTrue(result.isError());
105    }
106
107    public void testInvalidUri() {
108        Contact result = assertLoadContact(Uri.parse("content://wtf"));
109        assertTrue(result.isError());
110    }
111
112    public void testLoadContactWithContactIdUri() {
113        // Use content Uris that only contain the ID
114        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
115        final Uri entityUri = Uri.withAppendedPath(baseUri, Contacts.Entity.CONTENT_DIRECTORY);
116        final Uri lookupUri = ContentUris.withAppendedId(
117                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
118                CONTACT_ID);
119
120        ContactQueries queries = new ContactQueries();
121        mContactsProvider.expectTypeQuery(baseUri, Contacts.CONTENT_ITEM_TYPE);
122        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
123
124        Contact contact = assertLoadContact(baseUri);
125
126        assertEquals(CONTACT_ID, contact.getId());
127        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
128        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
129        assertEquals(LOOKUP_KEY, contact.getLookupKey());
130        assertEquals(lookupUri, contact.getLookupUri());
131        assertEquals(1, contact.getRawContacts().size());
132        assertEquals(1, contact.getStatuses().size());
133        mContactsProvider.verify();
134    }
135
136    public void testLoadContactWithOldStyleUri() {
137        // Use content Uris that only contain the ID but use the format used in Donut
138        final Uri legacyUri = ContentUris.withAppendedId(
139                Uri.parse("content://contacts"), RAW_CONTACT_ID);
140        final Uri rawContactUri = ContentUris.withAppendedId(
141                RawContacts.CONTENT_URI, RAW_CONTACT_ID);
142        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
143        final Uri lookupUri = ContentUris.withAppendedId(
144                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
145                CONTACT_ID);
146        final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
147
148        ContactQueries queries = new ContactQueries();
149        queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, CONTACT_ID, LOOKUP_KEY);
150        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
151
152        Contact contact = assertLoadContact(legacyUri);
153
154        assertEquals(CONTACT_ID, contact.getId());
155        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
156        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
157        assertEquals(LOOKUP_KEY, contact.getLookupKey());
158        assertEquals(lookupUri, contact.getLookupUri());
159        assertEquals(1, contact.getRawContacts().size());
160        assertEquals(1, contact.getStatuses().size());
161        if (CompatUtils.isMarshmallowCompatible()) {
162            assertEquals(
163                    1, contact.getRawContacts().get(0).getDataItems().get(0).getCarrierPresence());
164        }
165        mContactsProvider.verify();
166    }
167
168    public void testLoadContactWithRawContactIdUri() {
169        // Use content Uris that only contain the ID but use the format used in Donut
170        final Uri rawContactUri = ContentUris.withAppendedId(
171                RawContacts.CONTENT_URI, RAW_CONTACT_ID);
172        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
173        final Uri lookupUri = ContentUris.withAppendedId(
174                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
175                CONTACT_ID);
176        final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
177
178        ContactQueries queries = new ContactQueries();
179        mContactsProvider.expectTypeQuery(rawContactUri, RawContacts.CONTENT_ITEM_TYPE);
180        queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, CONTACT_ID, LOOKUP_KEY);
181        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
182
183        Contact contact = assertLoadContact(rawContactUri);
184
185        assertEquals(CONTACT_ID, contact.getId());
186        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
187        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
188        assertEquals(LOOKUP_KEY, contact.getLookupKey());
189        assertEquals(lookupUri, contact.getLookupUri());
190        assertEquals(1, contact.getRawContacts().size());
191        assertEquals(1, contact.getStatuses().size());
192        mContactsProvider.verify();
193    }
194
195    public void testLoadContactWithContactLookupUri() {
196        // Use lookup-style Uris that do not contain the Contact-ID
197        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
198        final Uri lookupNoIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY);
199        final Uri lookupUri = ContentUris.withAppendedId(lookupNoIdUri, CONTACT_ID);
200        final Uri entityUri = Uri.withAppendedPath(
201                lookupNoIdUri, Contacts.Entity.CONTENT_DIRECTORY);
202
203        ContactQueries queries = new ContactQueries();
204        mContactsProvider.expectTypeQuery(lookupNoIdUri, Contacts.CONTENT_ITEM_TYPE);
205        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
206
207        Contact contact = assertLoadContact(lookupNoIdUri);
208
209        assertEquals(CONTACT_ID, contact.getId());
210        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
211        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
212        assertEquals(LOOKUP_KEY, contact.getLookupKey());
213        assertEquals(lookupUri, contact.getLookupUri());
214        assertEquals(1, contact.getRawContacts().size());
215        assertEquals(1, contact.getStatuses().size());
216        mContactsProvider.verify();
217    }
218
219    public void testLoadContactWithContactLookupAndIdUri() {
220        // Use lookup-style Uris that also contain the Contact-ID
221        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
222        final Uri lookupUri = ContentUris.withAppendedId(
223                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
224                CONTACT_ID);
225        final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
226
227        ContactQueries queries = new ContactQueries();
228        mContactsProvider.expectTypeQuery(lookupUri, Contacts.CONTENT_ITEM_TYPE);
229        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
230
231        Contact contact = assertLoadContact(lookupUri);
232
233        assertEquals(CONTACT_ID, contact.getId());
234        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
235        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
236        assertEquals(LOOKUP_KEY, contact.getLookupKey());
237        assertEquals(lookupUri, contact.getLookupUri());
238        assertEquals(1, contact.getRawContacts().size());
239        assertEquals(1, contact.getStatuses().size());
240        mContactsProvider.verify();
241    }
242
243    public void testLoadContactWithContactLookupWithIncorrectIdUri() {
244        // Use lookup-style Uris that contain incorrect Contact-ID
245        // (we want to ensure that still the correct contact is chosen)
246        final long wrongContactId = 2;
247        final long wrongRawContactId = 12;
248
249        final String wrongLookupKey = "ab%12%@!";
250        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
251        final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
252        final Uri lookupUri = ContentUris.withAppendedId(
253                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
254                CONTACT_ID);
255        final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
256                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
257                wrongContactId);
258        final Uri entityUri = Uri.withAppendedPath(lookupWithWrongIdUri,
259                Contacts.Entity.CONTENT_DIRECTORY);
260
261        ContactQueries queries = new ContactQueries();
262        mContactsProvider.expectTypeQuery(lookupWithWrongIdUri, Contacts.CONTENT_ITEM_TYPE);
263        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
264
265        Contact contact = assertLoadContact(lookupWithWrongIdUri);
266
267        assertEquals(CONTACT_ID, contact.getId());
268        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
269        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
270        assertEquals(LOOKUP_KEY, contact.getLookupKey());
271        assertEquals(lookupUri, contact.getLookupUri());
272        assertEquals(1, contact.getRawContacts().size());
273        assertEquals(1, contact.getStatuses().size());
274
275        mContactsProvider.verify();
276    }
277
278    public void testLoadContactReturnDirectoryContactWithoutDisplayName() throws JSONException {
279        // Use lookup-style Uri that contains encoded json object which encapsulates the
280        // directory contact. The test json object is:
281        // {
282        //   display_name_source": 40,
283        //   "vnd.android.cursor.item\/contact":{"email":{"data1":"test@google.com" }}
284        // }
285        JSONObject itemJson = new JSONObject();
286        itemJson.put("email", new JSONObject().put("data1", "test@google.com"));
287        JSONObject json = new JSONObject();
288        json.put(Contacts.NAME_RAW_CONTACT_ID, CONTACT_ID);
289        json.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
290        json.put(Contacts.CONTENT_ITEM_TYPE, itemJson);
291
292        final Uri lookupUri = Contacts.CONTENT_LOOKUP_URI.buildUpon()
293                .encodedFragment(json.toString())
294                .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, "1")
295                .appendPath(Constants.LOOKUP_URI_ENCODED).build();
296
297        mContactsProvider.expectTypeQuery(lookupUri, Contacts.CONTENT_ITEM_TYPE);
298        Contact contact = assertLoadContact(lookupUri);
299
300        assertEquals(-1, contact.getId());
301        assertEquals(-1, contact.getNameRawContactId());
302        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
303        assertEquals("", contact.getDisplayName());
304        assertEquals(lookupUri, contact.getLookupUri());
305        assertEquals(1, contact.getRawContacts().size());
306        mContactsProvider.verify();
307    }
308
309    class ContactQueries {
310        public void fetchAllData(
311                Uri baseUri, long contactId, long rawContactId, long dataId, String encodedLookup) {
312            final String[] COLUMNS_INTERNAL = new String[] {
313                    Contacts.NAME_RAW_CONTACT_ID, Contacts.DISPLAY_NAME_SOURCE,
314                    Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME,
315                    Contacts.DISPLAY_NAME_ALTERNATIVE, Contacts.PHONETIC_NAME,
316                    Contacts.PHOTO_ID, Contacts.STARRED, Contacts.CONTACT_PRESENCE,
317                    Contacts.CONTACT_STATUS, Contacts.CONTACT_STATUS_TIMESTAMP,
318                    Contacts.CONTACT_STATUS_RES_PACKAGE, Contacts.CONTACT_STATUS_LABEL,
319
320                    Contacts.Entity.CONTACT_ID,
321                    Contacts.Entity.RAW_CONTACT_ID,
322
323                    RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
324                    RawContacts.DATA_SET,
325                    RawContacts.DIRTY, RawContacts.VERSION, RawContacts.SOURCE_ID,
326                    RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3, RawContacts.SYNC4,
327                    RawContacts.DELETED,
328
329                    Contacts.Entity.DATA_ID,
330
331                    Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
332                    Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10,
333                    Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
334                    Data.SYNC1, Data.SYNC2, Data.SYNC3, Data.SYNC4,
335                    Data.DATA_VERSION, Data.IS_PRIMARY,
336                    Data.IS_SUPER_PRIMARY, Data.MIMETYPE,
337
338                    GroupMembership.GROUP_SOURCE_ID,
339
340                    Data.PRESENCE, Data.CHAT_CAPABILITY,
341                    Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
342                    Data.STATUS_LABEL, Data.STATUS_TIMESTAMP,
343
344                    Contacts.PHOTO_URI,
345
346                    Contacts.SEND_TO_VOICEMAIL,
347                    Contacts.CUSTOM_RINGTONE,
348                    Contacts.IS_USER_PROFILE,
349
350                    Data.TIMES_USED,
351                    Data.LAST_TIME_USED
352            };
353
354            List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
355            if (CompatUtils.isMarshmallowCompatible()) {
356                projectionList.add(Data.CARRIER_PRESENCE);
357            }
358            final String[] COLUMNS = projectionList.toArray(new String[projectionList.size()]);
359
360            final Object[] ROWS_INTERNAL = new Object[] {
361                    rawContactId, 40,
362                    "aa%12%@!", "John Doe", "Doe, John", "jdo",
363                    0, 0, StatusUpdates.AVAILABLE,
364                    "Having lunch", 0,
365                    "mockPkg1", 10,
366
367                    contactId,
368                    rawContactId,
369
370                    "mockAccountName", "mockAccountType", null,
371                    0, 1, 0,
372                    "sync1", "sync2", "sync3", "sync4",
373                    0,
374
375                    dataId,
376
377                    "dat1", "dat2", "dat3", "dat4", "dat5",
378                    "dat6", "dat7", "dat8", "dat9", "dat10",
379                    "dat11", "dat12", "dat13", "dat14", "dat15",
380                    "syn1", "syn2", "syn3", "syn4",
381
382                    0, 0,
383                    0, StructuredName.CONTENT_ITEM_TYPE,
384
385                    "groupId",
386
387                    StatusUpdates.INVISIBLE, null,
388                    "Having dinner", "mockPkg3", 0,
389                    20, 0,
390
391                    "content:some.photo.uri",
392
393                    0,
394                    null,
395                    0,
396
397                    0,
398                    0
399            };
400
401            List<Object> rowsList = Lists.newArrayList(ROWS_INTERNAL);
402            if (CompatUtils.isMarshmallowCompatible()) {
403                rowsList.add(Data.CARRIER_PRESENCE_VT_CAPABLE);
404            }
405            final Object[] ROWS = rowsList.toArray(new Object[rowsList.size()]);
406
407            mContactsProvider.expectQuery(baseUri)
408                    .withProjection(COLUMNS)
409                    .withSortOrder(Contacts.Entity.RAW_CONTACT_ID)
410                    .returnRow(ROWS);
411        }
412
413        void fetchLookupAndId(final Uri sourceUri, final long expectedContactId,
414                final String expectedEncodedLookup) {
415            mContactsProvider.expectQuery(sourceUri)
416                    .withProjection(Contacts.LOOKUP_KEY, Contacts._ID)
417                    .returnRow(expectedEncodedLookup, expectedContactId);
418        }
419
420        void fetchContactIdAndLookupFromRawContactUri(final Uri rawContactUri,
421                final long expectedContactId, final String expectedEncodedLookup) {
422            // TODO: use a lighter query by joining rawcontacts with contacts in provider
423            // (See ContactContracts.java)
424            final Uri dataUri = Uri.withAppendedPath(rawContactUri,
425                    RawContacts.Data.CONTENT_DIRECTORY);
426            mContactsProvider.expectQuery(dataUri)
427                    .withProjection(RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY)
428                    .returnRow(expectedContactId, expectedEncodedLookup);
429        }
430    }
431}
432