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.providers.contacts;
18
19import com.android.internal.telephony.CallerInfo;
20import com.android.internal.telephony.PhoneConstants;
21import com.android.providers.contacts.testutil.CommonDatabaseUtils;
22
23import android.content.ComponentName;
24import android.content.ContentProvider;
25import android.content.ContentUris;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.ContextWrapper;
29import android.content.Intent;
30import android.content.pm.PackageManager;
31import android.database.Cursor;
32import android.database.MatrixCursor;
33import android.net.Uri;
34import android.provider.CallLog;
35import android.provider.CallLog.Calls;
36import android.provider.ContactsContract;
37import android.provider.ContactsContract.CommonDataKinds.Phone;
38import android.provider.VoicemailContract.Voicemails;
39import android.telecom.PhoneAccountHandle;
40import android.test.suitebuilder.annotation.MediumTest;
41
42import java.util.Arrays;
43import java.util.List;
44
45/**
46 * Unit tests for {@link CallLogProvider}.
47 *
48 * Run the test like this:
49 * <code>
50 * adb shell am instrument -e class com.android.providers.contacts.CallLogProviderTest -w \
51 *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
52 * </code>
53 */
54@MediumTest
55public class CallLogProviderTest extends BaseContactsProvider2Test {
56    /** Fields specific to voicemail provider that should not be exposed by call_log*/
57    private static final String[] VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS = new String[] {
58            Voicemails._DATA,
59            Voicemails.HAS_CONTENT,
60            Voicemails.MIME_TYPE,
61            Voicemails.SOURCE_PACKAGE,
62            Voicemails.SOURCE_DATA,
63            Voicemails.STATE,
64            Voicemails.DIRTY,
65            Voicemails.DELETED};
66    /** Total number of columns exposed by call_log provider. */
67    private static final int NUM_CALLLOG_FIELDS = 26;
68
69    private CallLogProvider mCallLogProvider;
70
71    @Override
72    protected Class<? extends ContentProvider> getProviderClass() {
73       return SynchronousContactsProvider2.class;
74    }
75
76    @Override
77    protected String getAuthority() {
78        return ContactsContract.AUTHORITY;
79    }
80
81    @Override
82    protected void setUp() throws Exception {
83        super.setUp();
84        mCallLogProvider = (CallLogProvider) addProvider(TestCallLogProvider.class,
85                CallLog.AUTHORITY);
86    }
87
88    @Override
89    protected void tearDown() throws Exception {
90        setUpWithVoicemailPermissions();
91        mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
92        super.tearDown();
93    }
94
95    public void testInsert_RegularCallRecord() {
96        ContentValues values = getDefaultCallValues();
97        Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
98        values.put(Calls.COUNTRY_ISO, "us");
99        assertStoredValues(uri, values);
100        assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
101    }
102
103    private void setUpWithVoicemailPermissions() {
104        mActor.addPermissions(ADD_VOICEMAIL_PERMISSION);
105        mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
106        mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
107    }
108
109    public void testInsert_VoicemailCallRecord() {
110        setUpWithVoicemailPermissions();
111        final ContentValues values = getDefaultCallValues();
112        values.put(Calls.TYPE, Calls.VOICEMAIL_TYPE);
113        values.put(Calls.VOICEMAIL_URI, "content://foo/voicemail/2");
114
115        // Should fail with the base content uri without the voicemail param.
116        EvenMoreAsserts.assertThrows(IllegalArgumentException.class, new Runnable() {
117            @Override
118            public void run() {
119                mResolver.insert(Calls.CONTENT_URI, values);
120            }
121        });
122
123        // Now grant voicemail permission - should succeed.
124        Uri uri  = mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, values);
125        assertStoredValues(uri, values);
126        assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
127    }
128
129    public void testUpdate() {
130        Uri uri = insertCallRecord();
131        ContentValues values = new ContentValues();
132        values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
133        values.put(Calls.NUMBER, "1-800-263-7643");
134        values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
135        values.put(Calls.DATE, 2000);
136        values.put(Calls.DURATION, 40);
137        values.put(Calls.CACHED_NAME, "1-800-GOOG-411");
138        values.put(Calls.CACHED_NUMBER_TYPE, Phone.TYPE_CUSTOM);
139        values.put(Calls.CACHED_NUMBER_LABEL, "Directory");
140
141        int count = mResolver.update(uri, values, null, null);
142        assertEquals(1, count);
143        assertStoredValues(uri, values);
144    }
145
146    public void testDelete() {
147        Uri uri = insertCallRecord();
148        try {
149            mResolver.delete(uri, null, null);
150            fail();
151        } catch (UnsupportedOperationException ex) {
152            // Expected
153        }
154
155        int count = mResolver.delete(Calls.CONTENT_URI, Calls._ID + "="
156                + ContentUris.parseId(uri), null);
157        assertEquals(1, count);
158        assertEquals(0, getCount(uri, null, null));
159    }
160
161    public void testCallLogFilter() {
162        ContentValues values = getDefaultCallValues();
163        mResolver.insert(Calls.CONTENT_URI, values);
164
165        Uri filterUri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, "1-800-4664-411");
166        Cursor c = mResolver.query(filterUri, null, null, null, null);
167        assertEquals(1, c.getCount());
168        c.moveToFirst();
169        assertCursorValues(c, values);
170        c.close();
171
172        filterUri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, "1-888-4664-411");
173        c = mResolver.query(filterUri, null, null, null, null);
174        assertEquals(0, c.getCount());
175        c.close();
176    }
177
178    public void testAddCall() {
179        CallerInfo ci = new CallerInfo();
180        ci.name = "1-800-GOOG-411";
181        ci.numberType = Phone.TYPE_CUSTOM;
182        ci.numberLabel = "Directory";
183        final ComponentName sComponentName = new ComponentName(
184                "com.android.server.telecom",
185                "TelecomServiceImpl");
186        PhoneAccountHandle subscription = new PhoneAccountHandle(
187                sComponentName, "sub0");
188
189        Uri uri = Calls.addCall(ci, getMockContext(), "1-800-263-7643",
190                PhoneConstants.PRESENTATION_ALLOWED, Calls.OUTGOING_TYPE, 0, subscription, 2000,
191                40, null);
192
193        ContentValues values = new ContentValues();
194        values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
195        values.put(Calls.FEATURES, 0);
196        values.put(Calls.NUMBER, "1-800-263-7643");
197        values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
198        values.put(Calls.DATE, 2000);
199        values.put(Calls.DURATION, 40);
200        // Cached values should not be updated immediately by the framework when inserting the call.
201        values.put(Calls.CACHED_NAME, (String) null);
202        values.put(Calls.CACHED_NUMBER_TYPE, (String) null);
203        values.put(Calls.CACHED_NUMBER_LABEL, (String) null);
204        values.put(Calls.COUNTRY_ISO, "us");
205        values.put(Calls.GEOCODED_LOCATION, "usa");
206        values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME,
207                "com.android.server.telecom/TelecomServiceImpl");
208        values.put(Calls.PHONE_ACCOUNT_ID, "sub0");
209        // Casting null to Long as there are many forms of "put" which have nullable second
210        // parameters and the compiler needs a hint as to which form is correct.
211        values.put(Calls.DATA_USAGE, (Long) null);
212        assertStoredValues(uri, values);
213    }
214
215    // Test to check that the calls and voicemail uris returns expected results.
216    public void testDifferentContentUris() {
217        setUpWithVoicemailPermissions();
218        // Insert one voicemaail and two regular call record.
219        insertVoicemailRecord();
220        insertCallRecord();
221        insertCallRecord();
222
223        // With the default uri, only 2 call entries should be returned.
224        // With the voicemail uri all 3 should be returned.
225        assertEquals(2, getCount(Calls.CONTENT_URI, null, null));
226        assertEquals(3, getCount(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null));
227    }
228
229    public void testLimitParamReturnsCorrectLimit() {
230        for (int i=0; i<10; i++) {
231            insertCallRecord();
232        }
233        Uri uri = Calls.CONTENT_URI.buildUpon()
234                .appendQueryParameter(Calls.LIMIT_PARAM_KEY, "4")
235                .build();
236        assertEquals(4, getCount(uri, null, null));
237    }
238
239    public void testLimitAndOffsetParamReturnsCorrectEntries() {
240        for (int i=0; i<10; i++) {
241            mResolver.insert(Calls.CONTENT_URI, getDefaultValues(Calls.INCOMING_TYPE));
242        }
243        for (int i=0; i<10; i++) {
244            mResolver.insert(Calls.CONTENT_URI, getDefaultValues(Calls.MISSED_TYPE));
245        }
246        // Limit 4 records.  Discard first 8.
247        Uri uri = Calls.CONTENT_URI.buildUpon()
248                .appendQueryParameter(Calls.LIMIT_PARAM_KEY, "4")
249                .appendQueryParameter(Calls.OFFSET_PARAM_KEY, "8")
250                .build();
251        String[] projection = new String[] {Calls._ID, Calls.TYPE};
252        Cursor c = mResolver.query(uri, projection, null, null, null);
253        try {
254            // First two should be incoming, next two should be missed.
255            for (int i = 0; i < 2; i++) {
256                c.moveToNext();
257                assertEquals(Calls.INCOMING_TYPE, c.getInt(1));
258            }
259            for (int i = 0; i < 2; i++) {
260                c.moveToNext();
261                assertEquals(Calls.MISSED_TYPE, c.getInt(1));
262            }
263        } finally {
264            c.close();
265        }
266    }
267
268    public void testUriWithBadLimitParamThrowsException() {
269        assertParamThrowsIllegalArgumentException(Calls.LIMIT_PARAM_KEY, "notvalid");
270    }
271
272    public void testUriWithBadOffsetParamThrowsException() {
273        assertParamThrowsIllegalArgumentException(Calls.OFFSET_PARAM_KEY, "notvalid");
274    }
275
276    private void assertParamThrowsIllegalArgumentException(String key, String value) {
277        Uri uri = Calls.CONTENT_URI.buildUpon()
278                .appendQueryParameter(key, value)
279                .build();
280        try {
281            mResolver.query(uri, null, null, null, null);
282            fail();
283        } catch (IllegalArgumentException e) {
284            assertTrue("Error does not contain value in question.",
285                    e.toString().contains(value));
286        }
287    }
288
289    // Test to check that none of the voicemail provider specific fields are
290    // insertable through call_log provider.
291    public void testCannotAccessVoicemailSpecificFields_Insert() {
292        for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
293            final ContentValues values = getDefaultCallValues();
294            values.put(voicemailColumn, "foo");
295            EvenMoreAsserts.assertThrows("Column: " + voicemailColumn,
296                    IllegalArgumentException.class, new Runnable() {
297                    @Override
298                    public void run() {
299                        mResolver.insert(Calls.CONTENT_URI, values);
300                    }
301                });
302        }
303    }
304
305    // Test to check that none of the voicemail provider specific fields are
306    // exposed through call_log provider query.
307    public void testCannotAccessVoicemailSpecificFields_Query() {
308        // Query.
309        Cursor cursor = mResolver.query(Calls.CONTENT_URI, null, null, null, null);
310        List<String> columnNames = Arrays.asList(cursor.getColumnNames());
311        assertEquals(NUM_CALLLOG_FIELDS, columnNames.size());
312        // None of the voicemail provider specific columns should be present.
313        for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
314            assertFalse("Unexpected column: '" + voicemailColumn + "' returned.",
315                    columnNames.contains(voicemailColumn));
316        }
317    }
318
319    // Test to check that none of the voicemail provider specific fields are
320    // updatable through call_log provider.
321    public void testCannotAccessVoicemailSpecificFields_Update() {
322        for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
323            final Uri insertedUri = insertCallRecord();
324            final ContentValues values = new ContentValues();
325            values.put(voicemailColumn, "foo");
326            EvenMoreAsserts.assertThrows("Column: " + voicemailColumn,
327                    IllegalArgumentException.class, new Runnable() {
328                    @Override
329                    public void run() {
330                        mResolver.update(insertedUri, values, null, null);
331                    }
332                });
333        }
334    }
335
336    public void testVoicemailPermissions_Insert() {
337        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
338            @Override
339            public void run() {
340                mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues());
341            }
342        });
343        // Should now succeed with permissions granted.
344        setUpWithVoicemailPermissions();
345        mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues());
346    }
347
348    public void testVoicemailPermissions_Update() {
349        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
350            @Override
351            public void run() {
352                mResolver.update(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues(),
353                        null, null);
354            }
355        });
356
357        // Should succeed with manage permission granted
358        mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
359        mResolver.update(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultCallValues(), null, null);
360        mActor.removePermissions(WRITE_VOICEMAIL_PERMISSION);
361
362        // Should also succeed with full permissions granted.
363        setUpWithVoicemailPermissions();
364        mResolver.update(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultCallValues(), null, null);
365    }
366
367    public void testVoicemailPermissions_Query() {
368        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
369            @Override
370            public void run() {
371                mResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null, null, null);
372            }
373        });
374
375        // Should succeed with read_all permission granted
376        mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
377        mResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null, null, null);
378        mActor.removePermissions(READ_VOICEMAIL_PERMISSION);
379
380        // Should also succeed with full permissions granted.
381        setUpWithVoicemailPermissions();
382        mResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null, null, null);
383    }
384
385    public void testVoicemailPermissions_Delete() {
386        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
387            @Override
388            public void run() {
389                mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
390            }
391        });
392
393        // Should succeed with manage permission granted
394        mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
395        mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
396        mActor.removePermissions(WRITE_VOICEMAIL_PERMISSION);
397
398        // Should now succeed with permissions granted.
399        setUpWithVoicemailPermissions();
400        mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
401    }
402
403    public void testCopyEntriesFromCursor_ReturnsMostRecentEntryTimestamp() {
404        assertEquals(10, mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor()));
405    }
406
407    public void testCopyEntriesFromCursor_AllEntriesSyncedWithoutDuplicatesPresent() {
408        assertStoredValues(Calls.CONTENT_URI);
409        mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor());
410        assertStoredValues(Calls.CONTENT_URI,
411                getTestCallLogValues(2),
412                getTestCallLogValues(1),
413                getTestCallLogValues(0));
414    }
415
416    public void testCopyEntriesFromCursor_DuplicatesIgnoredCorrectly() {
417        mResolver.insert(Calls.CONTENT_URI, getTestCallLogValues(1));
418        assertStoredValues(Calls.CONTENT_URI, getTestCallLogValues(1));
419        mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor());
420        assertStoredValues(Calls.CONTENT_URI,
421                getTestCallLogValues(2),
422                getTestCallLogValues(1),
423                getTestCallLogValues(0));
424    }
425
426    private ContentValues getDefaultValues(int callType) {
427        ContentValues values = new ContentValues();
428        values.put(Calls.TYPE, callType);
429        values.put(Calls.NUMBER, "1-800-4664-411");
430        values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
431        values.put(Calls.DATE, 1000);
432        values.put(Calls.DURATION, 30);
433        values.put(Calls.NEW, 1);
434        return values;
435    }
436
437    private ContentValues getDefaultCallValues() {
438        return getDefaultValues(Calls.INCOMING_TYPE);
439    }
440
441    private ContentValues getDefaultVoicemailValues() {
442        return getDefaultValues(Calls.VOICEMAIL_TYPE);
443    }
444
445    private Uri insertCallRecord() {
446        return mResolver.insert(Calls.CONTENT_URI, getDefaultCallValues());
447    }
448
449    private Uri insertVoicemailRecord() {
450        return mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues());
451    }
452
453    public static class TestCallLogProvider extends CallLogProvider {
454        private static ContactsDatabaseHelper mDbHelper;
455
456        @Override
457        protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
458            if (mDbHelper == null) {
459                mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
460            }
461            return mDbHelper;
462        }
463
464        @Override
465        protected CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
466            return new CallLogInsertionHelper() {
467                @Override
468                public String getGeocodedLocationFor(String number, String countryIso) {
469                    return "usa";
470                }
471
472                @Override
473                public void addComputedValues(ContentValues values) {
474                    values.put(Calls.COUNTRY_ISO, "us");
475                    values.put(Calls.GEOCODED_LOCATION, "usa");
476                }
477            };
478        }
479
480        @Override
481        protected Context context() {
482            return new ContextWrapper(super.context()) {
483                @Override
484                public PackageManager getPackageManager() {
485                    return super.getPackageManager();
486                }
487
488                @Override
489                public void sendBroadcast(Intent intent, String receiverPermission) {
490                   // Do nothing for now.
491                }
492            };
493        }
494    }
495
496    private Cursor getTestCallLogCursor() {
497        final MatrixCursor cursor = new MatrixCursor(CallLogProvider.CALL_LOG_SYNC_PROJECTION);
498        for (int i = 2; i >= 0; i--) {
499            cursor.addRow(CommonDatabaseUtils.getArrayFromContentValues(getTestCallLogValues(i),
500                    CallLogProvider.CALL_LOG_SYNC_PROJECTION));
501        }
502        return cursor;
503    }
504
505    /**
506     * Returns a predefined {@link ContentValues} object based on the provided index.
507     */
508    private ContentValues getTestCallLogValues(int i) {
509        ContentValues values = new ContentValues();
510        switch (i) {
511            case 0:
512                values.put(Calls.NUMBER, "123456");
513                values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
514                values.put(Calls.TYPE, Calls.MISSED_TYPE);
515                values.put(Calls.FEATURES, 0);
516                values.put(Calls.DATE, 10);
517                values.put(Calls.DURATION, 100);
518                values.put(Calls.DATA_USAGE, 1000);
519                values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, (String) null);
520                values.put(Calls.PHONE_ACCOUNT_ID, (Long) null);
521                break;
522            case 1:
523                values.put(Calls.NUMBER, "654321");
524                values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
525                values.put(Calls.TYPE, Calls.INCOMING_TYPE);
526                values.put(Calls.FEATURES, 0);
527                values.put(Calls.DATE, 5);
528                values.put(Calls.DURATION, 200);
529                values.put(Calls.DATA_USAGE, 0);
530                values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, (String) null);
531                values.put(Calls.PHONE_ACCOUNT_ID, (Long) null);
532                break;
533            case 2:
534                values.put(Calls.NUMBER, "123456");
535                values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
536                values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
537                values.put(Calls.FEATURES, Calls.FEATURES_VIDEO);
538                values.put(Calls.DATE, 1);
539                values.put(Calls.DURATION, 50);
540                values.put(Calls.DATA_USAGE, 2000);
541                values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, (String) null);
542                values.put(Calls.PHONE_ACCOUNT_ID, (Long) null);
543                break;
544        }
545        return values;
546    }
547}
548