1/*
2 * Copyright (C) 2011 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 android.content.ContentUris;
20import android.content.ContentValues;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.ParcelFileDescriptor;
24import android.provider.CallLog.Calls;
25import android.provider.VoicemailContract;
26import android.provider.VoicemailContract.Status;
27import android.provider.VoicemailContract.Voicemails;
28import android.test.MoreAsserts;
29import android.test.suitebuilder.annotation.SmallTest;
30
31import com.android.common.io.MoreCloseables;
32
33import java.io.FileNotFoundException;
34import java.io.IOException;
35import java.io.InputStream;
36import java.io.OutputStream;
37import java.util.Arrays;
38import java.util.List;
39
40/**
41 * Unit tests for {@link VoicemailContentProvider}.
42 *
43 * Run the test like this:
44 * <code>
45 * runtest -c com.android.providers.contacts.VoicemailProviderTest contactsprov
46 * </code>
47 */
48// TODO: Test that calltype and voicemail_uri are auto populated by the provider.
49@SmallTest
50public class VoicemailProviderTest extends BaseVoicemailProviderTest {
51    /** Fields specific to call_log provider that should not be exposed by voicemail provider. */
52    private static final String[] CALLLOG_PROVIDER_SPECIFIC_COLUMNS = {
53            Calls.CACHED_NAME,
54            Calls.CACHED_NUMBER_LABEL,
55            Calls.CACHED_NUMBER_TYPE,
56            Calls.TYPE,
57            Calls.VOICEMAIL_URI,
58            Calls.COUNTRY_ISO
59    };
60    /** Total number of columns exposed by voicemail provider. */
61    private static final int NUM_VOICEMAIL_FIELDS = 14;
62
63    @Override
64    protected void setUp() throws Exception {
65        super.setUp();
66        setUpForOwnPermission();
67    }
68
69    /** Returns the appropriate /voicemail URI. */
70    private Uri voicemailUri() {
71        return mUseSourceUri ?
72                Voicemails.buildSourceUri(mActor.packageName) : Voicemails.CONTENT_URI;
73    }
74
75    /** Returns the appropriate /status URI. */
76    private Uri statusUri() {
77        return mUseSourceUri ?
78                Status.buildSourceUri(mActor.packageName) : Status.CONTENT_URI;
79    }
80
81    public void testInsert() throws Exception {
82        Uri uri = mResolver.insert(voicemailUri(), getTestVoicemailValues());
83        // We create on purpose a new set of ContentValues here, because the code above modifies
84        // the copy it gets.
85        assertStoredValues(uri, getTestVoicemailValues());
86        assertSelection(uri, getTestVoicemailValues(), Voicemails._ID, ContentUris.parseId(uri));
87        assertEquals(1, countFilesInTestDirectory());
88    }
89
90    // Test to ensure that media content can be written and read back.
91    public void testFileContent() throws Exception {
92        Uri uri = insertVoicemail();
93        OutputStream out = mResolver.openOutputStream(uri);
94        byte[] outBuffer = {0x1, 0x2, 0x3, 0x4};
95        out.write(outBuffer);
96        out.flush();
97        out.close();
98        InputStream in = mResolver.openInputStream(uri);
99        byte[] inBuffer = new byte[4];
100        int numBytesRead = in.read(inBuffer);
101        assertEquals(numBytesRead, outBuffer.length);
102        MoreAsserts.assertEquals(outBuffer, inBuffer);
103        // No more data should be left.
104        assertEquals(-1, in.read(inBuffer));
105        in.close();
106    }
107
108    public void testUpdate() {
109        Uri uri = insertVoicemail();
110        ContentValues values = new ContentValues();
111        values.put(Voicemails.NUMBER, "1-800-263-7643");
112        values.put(Voicemails.DATE, 2000);
113        values.put(Voicemails.DURATION, 40);
114        values.put(Voicemails.TRANSCRIPTION, "Testing 123");
115        values.put(Voicemails.STATE, 2);
116        values.put(Voicemails.HAS_CONTENT, 1);
117        values.put(Voicemails.SOURCE_DATA, "foo");
118        int count = mResolver.update(uri, values, null, null);
119        assertEquals(1, count);
120        assertStoredValues(uri, values);
121    }
122
123    public void testDelete() {
124        Uri uri = insertVoicemail();
125        int count = mResolver.delete(voicemailUri(), Voicemails._ID + "="
126                + ContentUris.parseId(uri), null);
127        assertEquals(1, count);
128        assertEquals(0, getCount(uri, null, null));
129    }
130
131    public void testGetType_ItemUri() throws Exception {
132        // Random item uri.
133        assertEquals(Voicemails.ITEM_TYPE,
134                mResolver.getType(ContentUris.withAppendedId(Voicemails.CONTENT_URI, 100)));
135        // Item uri of an inserted voicemail.
136        ContentValues values = getTestVoicemailValues();
137        values.put(Voicemails.MIME_TYPE, "foo/bar");
138        Uri uri = mResolver.insert(voicemailUri(), values);
139        assertEquals(Voicemails.ITEM_TYPE, mResolver.getType(uri));
140    }
141
142    public void testGetType_DirUri() throws Exception {
143        assertEquals(Voicemails.DIR_TYPE, mResolver.getType(Voicemails.CONTENT_URI));
144        assertEquals(Voicemails.DIR_TYPE, mResolver.getType(Voicemails.buildSourceUri("foo")));
145    }
146
147    // Test to ensure that without full permission it is not possible to use the base uri (i.e. with
148    // no package URI specified).
149    public void testMustUsePackageUriWithoutFullPermission() {
150        setUpForOwnPermission();
151        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
152            @Override
153            public void run() {
154                mResolver.insert(Voicemails.CONTENT_URI, getTestVoicemailValues());
155            }
156        });
157
158        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
159            @Override
160            public void run() {
161                mResolver.update(Voicemails.CONTENT_URI, getTestVoicemailValues(), null, null);
162            }
163        });
164
165        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
166            @Override
167            public void run() {
168                mResolver.query(Voicemails.CONTENT_URI, null, null, null, null);
169            }
170        });
171
172        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
173            @Override
174            public void run() {
175                mResolver.delete(Voicemails.CONTENT_URI, null, null);
176            }
177        });
178    }
179
180    public void testPermissions_InsertAndQuery() {
181        setUpForFullPermission();
182        // Insert two records - one each with own and another package.
183        insertVoicemail();
184        insertVoicemailForSourcePackage("another-package");
185        assertEquals(2, getCount(voicemailUri(), null, null));
186
187        // Now give away full permission and check that only 1 message is accessible.
188        setUpForOwnPermission();
189        assertEquals(1, getCount(voicemailUri(), null, null));
190
191        // Once again try to insert message for another package. This time
192        // it should fail.
193        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
194            @Override
195            public void run() {
196                insertVoicemailForSourcePackage("another-package");
197            }
198        });
199
200        setUpForNoPermission();
201        mUseSourceUri = false;
202        // With the READ_ALL_VOICEMAIL permission, we should now be able to read all voicemails
203        mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
204        assertEquals(2, getCount(voicemailUri(), null, null));
205
206        // An insert for another package should still fail
207        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
208            @Override
209            public void run() {
210                insertVoicemailForSourcePackage("another-package");
211            }
212        });
213    }
214
215    public void testPermissions_UpdateAndDelete() {
216        setUpForFullPermission();
217        // Insert two records - one each with own and another package.
218        final Uri ownVoicemail = insertVoicemail();
219        final Uri anotherVoicemail = insertVoicemailForSourcePackage("another-package");
220        assertEquals(2, getCount(voicemailUri(), null, null));
221
222        // Now give away full permission and check that we can update and delete only
223        // the own voicemail.
224        setUpForOwnPermission();
225        mResolver.update(withSourcePackageParam(ownVoicemail),
226                getTestVoicemailValues(), null, null);
227        mResolver.delete(withSourcePackageParam(ownVoicemail), null, null);
228
229        // However, attempting to update or delete another-package's voicemail should fail.
230        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
231            @Override
232            public void run() {
233                mResolver.update(anotherVoicemail, null, null, null);
234            }
235        });
236        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
237            @Override
238            public void run() {
239                mResolver.delete(anotherVoicemail, null, null);
240            }
241        });
242
243        // If we have the manage voicemail permission, we should be able to both update and delete
244        // voicemails from all packages
245        setUpForNoPermission();
246        mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
247        mResolver.update(anotherVoicemail, getTestVoicemailValues(), null, null);
248
249        // Now add the read voicemail permission temporarily to verify that the update actually
250        // worked
251        mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
252        assertStoredValues(anotherVoicemail, getTestVoicemailValues());
253        mActor.removePermissions(READ_VOICEMAIL_PERMISSION);
254
255        mResolver.delete(anotherVoicemail, null, null);
256
257        // Now add the read voicemail permission temporarily to verify that the delete actually
258        // worked
259        mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
260        assertEquals(0, getCount(anotherVoicemail, null, null));
261    }
262
263    private Uri withSourcePackageParam(Uri uri) {
264        return uri.buildUpon()
265            .appendQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, mActor.packageName)
266            .build();
267    }
268
269    public void testUriPermissions() {
270        setUpForFullPermission();
271        final Uri uri1 = insertVoicemail();
272        final Uri uri2 = insertVoicemail();
273        // Give away all permissions before querying. Access to both uris should be denied.
274        setUpForNoPermission();
275        checkHasNoAccessToUri(uri1);
276        checkHasNoAccessToUri(uri2);
277
278        // Just grant permission to uri1. uri1 should pass but uri2 should still fail.
279        mActor.addUriPermissions(uri1);
280        checkHasReadOnlyAccessToUri(uri1);
281        checkHasNoAccessToUri(uri2);
282
283        // Cleanup.
284        mActor.removeUriPermissions(uri1);
285    }
286
287    /*
288     * Checks that the READ_ALL_VOICEMAIL permission provides read access to a uri.
289     */
290    public void testUriPermissions_ReadAccess() {
291        setUpForFullPermission();
292        final Uri uri1 = insertVoicemail();
293        // Give away all permissions before querying. Access should be denied.
294        setUpForNoPermission();
295        mUseSourceUri = false;
296        checkHasNoAccessToUri(uri1);
297
298        mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
299        checkHasReadAccessToUri(uri1);
300    }
301
302    /*
303     * Checks that the MANAGE_VOICEMAIL permission provides write access to a uri.
304     */
305    public void testUriPermissions_WriteAccess() {
306        setUpForFullPermission();
307        final Uri uri1 = insertVoicemail();
308        // Give away all permissions before querying. Access should be denied.
309        setUpForNoPermission();
310        checkHasNoAccessToUri(uri1);
311
312        mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
313        checkHasUpdateAndDeleteAccessToUri(uri1);
314    }
315
316    private void checkHasNoAccessToUri(final Uri uri) {
317        checkHasNoReadAccessToUri(uri);
318        checkHasNoWriteAccessToUri(uri);
319    }
320
321    private void checkHasReadOnlyAccessToUri(final Uri uri) {
322        checkHasReadAccessToUri(uri);
323        checkHasNoWriteAccessToUri(uri);
324    }
325
326    private void checkHasReadAccessToUri(final Uri uri) {
327        Cursor cursor = null;
328        try {
329            cursor = mResolver.query(uri, null, null ,null, null);
330            assertEquals(1, cursor.getCount());
331            try {
332                ParcelFileDescriptor fd = mResolver.openFileDescriptor(uri, "r");
333                assertNotNull(fd);
334                fd.close();
335            } catch (FileNotFoundException e) {
336                fail(e.getMessage());
337            } catch (IOException e) {
338                fail(e.getMessage());
339            }
340        } finally {
341            MoreCloseables.closeQuietly(cursor);
342        }
343    }
344
345    private void checkHasNoReadAccessToUri(final Uri uri) {
346        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
347            @Override
348            public void run() {
349                mResolver.query(uri, null, null ,null, null);
350            }
351        });
352        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
353            @Override
354            public void run() {
355                try {
356                    mResolver.openFileDescriptor(uri, "r");
357                } catch (FileNotFoundException e) {
358                    fail(e.getMessage());
359                }
360            }
361        });
362    }
363
364    private void checkHasUpdateAndDeleteAccessToUri(final Uri uri) {
365        mResolver.update(uri, getTestVoicemailValues(), null, null);
366        mResolver.delete(uri, null, null);
367    }
368
369    private void checkHasNoWriteAccessToUri(final Uri uri) {
370        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
371            @Override
372            public void run() {
373                mResolver.update(uri, getTestVoicemailValues(), null, null);
374            }
375        });
376        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
377            @Override
378            public void run() {
379                mResolver.delete(uri, null, null);
380            }
381        });
382        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
383            @Override
384            public void run() {
385                try {
386                    mResolver.openFileDescriptor(uri, "w");
387                } catch (FileNotFoundException e) {
388                    fail(e.getMessage());
389                }
390            }
391        });
392        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
393            @Override
394            public void run() {
395                try {
396                    mResolver.openFileDescriptor(uri, "rw");
397                } catch (FileNotFoundException e) {
398                    fail(e.getMessage());
399                }
400            }
401        });
402    }
403
404    // Test to ensure that all operations fail when no voicemail permission is granted.
405    public void testNoPermissions() {
406        setUpForNoPermission();
407        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
408            @Override
409            public void run() {
410                mResolver.insert(voicemailUri(), getTestVoicemailValues());
411            }
412        });
413        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
414            @Override
415            public void run() {
416                mResolver.update(voicemailUri(), getTestVoicemailValues(), null, null);
417            }
418        });
419        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
420            @Override
421            public void run() {
422                mResolver.query(voicemailUri(), null, null, null, null);
423            }
424        });
425        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
426            @Override
427            public void run() {
428                mResolver.delete(voicemailUri(), null, null);
429            }
430        });
431    }
432
433    // Test to check that none of the call_log provider specific fields are
434    // insertable through voicemail provider.
435    public void testCannotAccessCallLogSpecificFields_Insert() {
436        for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
437            final ContentValues values = getTestVoicemailValues();
438            values.put(callLogColumn, "foo");
439            EvenMoreAsserts.assertThrows("Column: " + callLogColumn,
440                    IllegalArgumentException.class, new Runnable() {
441                    @Override
442                    public void run() {
443                        mResolver.insert(voicemailUri(), values);
444                    }
445                });
446        }
447    }
448
449    // Test to check that none of the call_log provider specific fields are
450    // exposed through voicemail provider query.
451    public void testCannotAccessCallLogSpecificFields_Query() {
452        // Query.
453        Cursor cursor = mResolver.query(voicemailUri(), null, null, null, null);
454        List<String> columnNames = Arrays.asList(cursor.getColumnNames());
455        assertEquals(NUM_VOICEMAIL_FIELDS, columnNames.size());
456        // None of the call_log provider specific columns should be present.
457        for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
458            assertFalse("Unexpected column: '" + callLogColumn + "' returned.",
459                    columnNames.contains(callLogColumn));
460        }
461    }
462
463    // Test to check that none of the call_log provider specific fields are
464    // updatable through voicemail provider.
465    public void testCannotAccessCallLogSpecificFields_Update() {
466        for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
467            final Uri insertedUri = insertVoicemail();
468            final ContentValues values = getTestVoicemailValues();
469            values.put(callLogColumn, "foo");
470            EvenMoreAsserts.assertThrows("Column: " + callLogColumn,
471                    IllegalArgumentException.class, new Runnable() {
472                    @Override
473                    public void run() {
474                        mResolver.update(insertedUri, values, null, null);
475                    }
476                });
477        }
478    }
479
480    // Tests for voicemail status table.
481
482    public void testStatusInsert() throws Exception {
483        ContentValues values = getTestStatusValues();
484        Uri uri = mResolver.insert(statusUri(), values);
485        assertStoredValues(uri, values);
486        assertSelection(uri, values, Status._ID, ContentUris.parseId(uri));
487    }
488
489    // Test to ensure that duplicate entries for the same package still end up as the same record.
490    public void testStatusInsertDuplicate() throws Exception {
491        setUpForFullPermission();
492        ContentValues values = getTestStatusValues();
493        assertNotNull(mResolver.insert(statusUri(), values));
494        assertEquals(1, getCount(statusUri(), null, null));
495
496        // Insertion request for the same package should fail with no change in count.
497        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
498        assertNull(mResolver.insert(statusUri(), values));
499        assertEquals(1, getCount(statusUri(), null, null));
500
501        // Now insert entry for another source package, and it should end up as a separate record.
502        values.put(Status.SOURCE_PACKAGE, "another.package");
503        assertNotNull(mResolver.insert(statusUri(), values));
504        assertEquals(2, getCount(statusUri(), null, null));
505    }
506
507    public void testStatusUpdate() throws Exception {
508        Uri uri = insertTestStatusEntry();
509        ContentValues values = getTestStatusValues();
510        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
511        values.put(Status.NOTIFICATION_CHANNEL_STATE,
512                Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING);
513        int count = mResolver.update(uri, values, null, null);
514        assertEquals(1, count);
515        assertStoredValues(uri, values);
516    }
517
518    public void testStatusDelete() {
519        Uri uri = insertTestStatusEntry();
520        int count = mResolver.delete(statusUri(), Status._ID + "="
521                + ContentUris.parseId(uri), null);
522        assertEquals(1, count);
523        assertEquals(0, getCount(uri, null, null));
524    }
525
526    public void testStatusGetType() throws Exception {
527        // Item URI.
528        Uri uri = insertTestStatusEntry();
529        assertEquals(Status.ITEM_TYPE, mResolver.getType(uri));
530
531        // base URIs.
532        assertEquals(Status.DIR_TYPE, mResolver.getType(Status.CONTENT_URI));
533        assertEquals(Status.DIR_TYPE, mResolver.getType(Status.buildSourceUri("foo")));
534    }
535
536    // Basic permission checks for the status table.
537    public void testStatusPermissions() throws Exception {
538        final ContentValues values = getTestStatusValues();
539        // Inserting for another package should fail with any of the URIs.
540        values.put(Status.SOURCE_PACKAGE, "another.package");
541        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
542            @Override
543            public void run() {
544                mResolver.insert(Status.CONTENT_URI, values);
545            }
546        });
547        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
548            @Override
549            public void run() {
550                mResolver.insert(Status.buildSourceUri(mActor.packageName), values);
551            }
552        });
553
554        // But insertion with own package should succeed with the right uri.
555        values.put(Status.SOURCE_PACKAGE, mActor.packageName);
556        final Uri uri = mResolver.insert(Status.buildSourceUri(mActor.packageName), values);
557        assertNotNull(uri);
558
559        // Updating source_package should not work as well.
560        values.put(Status.SOURCE_PACKAGE, "another.package");
561        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
562            @Override
563            public void run() {
564                mResolver.update(uri, values, null, null);
565            }
566        });
567    }
568
569    // File operation is not supported by /status URI.
570    public void testStatusFileOperation() throws Exception {
571        final Uri uri = insertTestStatusEntry();
572        EvenMoreAsserts.assertThrows(UnsupportedOperationException.class, new Runnable() {
573            @Override
574            public void run() {
575                try {
576                    mResolver.openOutputStream(uri);
577                } catch (FileNotFoundException e) {
578                    fail("Unexpected exception " + e);
579                }
580            }
581        });
582
583        EvenMoreAsserts.assertThrows(UnsupportedOperationException.class, new Runnable() {
584            @Override
585            public void run() {
586                try {
587                    mResolver.openInputStream(uri);
588                } catch (FileNotFoundException e) {
589                    fail("Unexpected exception " + e);
590                }
591            }
592        });
593    }
594
595    /**
596     * Inserts a voicemail record with no source package set. The content provider
597     * will detect source package.
598     */
599    private Uri insertVoicemail() {
600        return mResolver.insert(voicemailUri(), getTestVoicemailValues());
601    }
602
603    /** Inserts a voicemail record for the specified source package. */
604    private Uri insertVoicemailForSourcePackage(String sourcePackage) {
605        ContentValues values = getTestVoicemailValues();
606        values.put(Voicemails.SOURCE_PACKAGE, sourcePackage);
607        return mResolver.insert(voicemailUri(), values);
608    }
609
610    private ContentValues getTestVoicemailValues() {
611        ContentValues values = new ContentValues();
612        values.put(Voicemails.NUMBER, "1-800-4664-411");
613        values.put(Voicemails.DATE, 1000);
614        values.put(Voicemails.DURATION, 30);
615        values.put(Voicemails.TRANSCRIPTION, "Testing 123");
616        values.put(Voicemails.IS_READ, 0);
617        values.put(Voicemails.HAS_CONTENT, 0);
618        values.put(Voicemails.SOURCE_DATA, "1234");
619        values.put(Voicemails.STATE, Voicemails.STATE_INBOX);
620        return values;
621    }
622
623    private Uri insertTestStatusEntry() {
624        return mResolver.insert(statusUri(), getTestStatusValues());
625    }
626
627    private ContentValues getTestStatusValues() {
628        ContentValues values = new ContentValues();
629        values.put(Status.SOURCE_PACKAGE, mActor.packageName);
630        values.put(Status.VOICEMAIL_ACCESS_URI, "tel:901");
631        values.put(Status.SETTINGS_URI, "com.example.voicemail.source.SettingsActivity");
632        values.put(Status.CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
633        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_OK);
634        values.put(Status.NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
635        return values;
636    }
637}
638