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 = 13;
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.STATE, 2);
115        values.put(Voicemails.HAS_CONTENT, 1);
116        values.put(Voicemails.SOURCE_DATA, "foo");
117        int count = mResolver.update(uri, values, null, null);
118        assertEquals(1, count);
119        assertStoredValues(uri, values);
120    }
121
122    public void testDelete() {
123        Uri uri = insertVoicemail();
124        int count = mResolver.delete(voicemailUri(), Voicemails._ID + "="
125                + ContentUris.parseId(uri), null);
126        assertEquals(1, count);
127        assertEquals(0, getCount(uri, null, null));
128    }
129
130    public void testGetType_ItemUri() throws Exception {
131        // Random item uri.
132        assertEquals(Voicemails.ITEM_TYPE,
133                mResolver.getType(ContentUris.withAppendedId(Voicemails.CONTENT_URI, 100)));
134        // Item uri of an inserted voicemail.
135        ContentValues values = getTestVoicemailValues();
136        values.put(Voicemails.MIME_TYPE, "foo/bar");
137        Uri uri = mResolver.insert(voicemailUri(), values);
138        assertEquals(Voicemails.ITEM_TYPE, mResolver.getType(uri));
139    }
140
141    public void testGetType_DirUri() throws Exception {
142        assertEquals(Voicemails.DIR_TYPE, mResolver.getType(Voicemails.CONTENT_URI));
143        assertEquals(Voicemails.DIR_TYPE, mResolver.getType(Voicemails.buildSourceUri("foo")));
144    }
145
146    // Test to ensure that without full permission it is not possible to use the base uri (i.e. with
147    // no package URI specified).
148    public void testMustUsePackageUriWithoutFullPermission() {
149        setUpForOwnPermission();
150        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
151            @Override
152            public void run() {
153                mResolver.insert(Voicemails.CONTENT_URI, getTestVoicemailValues());
154            }
155        });
156
157        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
158            @Override
159            public void run() {
160                mResolver.update(Voicemails.CONTENT_URI, getTestVoicemailValues(), null, null);
161            }
162        });
163
164        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
165            @Override
166            public void run() {
167                mResolver.query(Voicemails.CONTENT_URI, null, null, null, null);
168            }
169        });
170
171        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
172            @Override
173            public void run() {
174                mResolver.delete(Voicemails.CONTENT_URI, null, null);
175            }
176        });
177    }
178
179    public void testPermissions_InsertAndQuery() {
180        setUpForFullPermission();
181        // Insert two records - one each with own and another package.
182        insertVoicemail();
183        insertVoicemailForSourcePackage("another-package");
184        assertEquals(2, getCount(voicemailUri(), null, null));
185
186        // Now give away full permission and check that only 1 message is accessible.
187        setUpForOwnPermission();
188        assertEquals(1, getCount(voicemailUri(), null, null));
189
190        // Once again try to insert message for another package. This time
191        // it should fail.
192        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
193            @Override
194            public void run() {
195                insertVoicemailForSourcePackage("another-package");
196            }
197        });
198    }
199
200    public void testPermissions_UpdateAndDelete() {
201        setUpForFullPermission();
202        // Insert two records - one each with own and another package.
203        final Uri ownVoicemail = insertVoicemail();
204        final Uri anotherVoicemail = insertVoicemailForSourcePackage("another-package");
205        assertEquals(2, getCount(voicemailUri(), null, null));
206
207        // Now give away full permission and check that we can update and delete only
208        // the own voicemail.
209        setUpForOwnPermission();
210        mResolver.update(withSourcePackageParam(ownVoicemail),
211                getTestVoicemailValues(), null, null);
212        mResolver.delete(withSourcePackageParam(ownVoicemail), null, null);
213
214        // However, attempting to update or delete another-package's voicemail should fail.
215        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
216            @Override
217            public void run() {
218                mResolver.update(anotherVoicemail, null, null, null);
219            }
220        });
221        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
222            @Override
223            public void run() {
224                mResolver.delete(anotherVoicemail, null, null);
225            }
226        });
227    }
228
229    private Uri withSourcePackageParam(Uri uri) {
230        return uri.buildUpon()
231            .appendQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, mActor.packageName)
232            .build();
233    }
234
235    public void testUriPermissions() {
236        setUpForFullPermission();
237        final Uri uri1 = insertVoicemail();
238        final Uri uri2 = insertVoicemail();
239        // Give away all permissions before querying. Access to both uris should be denied.
240        setUpForNoPermission();
241        checkHasNoAccessToUri(uri1);
242        checkHasNoAccessToUri(uri2);
243
244        // Just grant permission to uri1. uri1 should pass but uri2 should still fail.
245        mActor.addUriPermissions(uri1);
246        checkHasReadOnlyAccessToUri(uri1);
247        checkHasNoAccessToUri(uri2);
248
249        // Cleanup.
250        mActor.removeUriPermissions(uri1);
251    }
252
253    private void checkHasNoAccessToUri(final Uri uri) {
254        checkHasNoReadAccessToUri(uri);
255        checkHasNoWriteAccessToUri(uri);
256    }
257
258    private void checkHasReadOnlyAccessToUri(final Uri uri) {
259        checkHasReadAccessToUri(uri);
260        checkHasNoWriteAccessToUri(uri);
261    }
262
263    private void checkHasReadAccessToUri(final Uri uri) {
264        Cursor cursor = null;
265        try {
266            cursor = mResolver.query(uri, null, null ,null, null);
267            assertEquals(1, cursor.getCount());
268            try {
269                ParcelFileDescriptor fd = mResolver.openFileDescriptor(uri, "r");
270                assertNotNull(fd);
271                fd.close();
272            } catch (FileNotFoundException e) {
273                fail(e.getMessage());
274            } catch (IOException e) {
275                fail(e.getMessage());
276            }
277        } finally {
278            MoreCloseables.closeQuietly(cursor);
279        }
280    }
281
282    private void checkHasNoReadAccessToUri(final Uri uri) {
283        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
284            @Override
285            public void run() {
286                mResolver.query(uri, null, null ,null, null);
287            }
288        });
289        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
290            @Override
291            public void run() {
292                try {
293                    mResolver.openFileDescriptor(uri, "r");
294                } catch (FileNotFoundException e) {
295                    fail(e.getMessage());
296                }
297            }
298        });
299    }
300
301    private void checkHasNoWriteAccessToUri(final Uri uri) {
302        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
303            @Override
304            public void run() {
305                mResolver.update(uri, getTestVoicemailValues(), null, null);
306            }
307        });
308        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
309            @Override
310            public void run() {
311                mResolver.delete(uri, null, null);
312            }
313        });
314        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
315            @Override
316            public void run() {
317                try {
318                    mResolver.openFileDescriptor(uri, "w");
319                } catch (FileNotFoundException e) {
320                    fail(e.getMessage());
321                }
322            }
323        });
324        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
325            @Override
326            public void run() {
327                try {
328                    mResolver.openFileDescriptor(uri, "rw");
329                } catch (FileNotFoundException e) {
330                    fail(e.getMessage());
331                }
332            }
333        });
334    }
335
336    // Test to ensure that all operations fail when no voicemail permission is granted.
337    public void testNoPermissions() {
338        setUpForNoPermission();
339        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
340            @Override
341            public void run() {
342                mResolver.insert(voicemailUri(), getTestVoicemailValues());
343            }
344        });
345        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
346            @Override
347            public void run() {
348                mResolver.update(voicemailUri(), getTestVoicemailValues(), null, null);
349            }
350        });
351        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
352            @Override
353            public void run() {
354                mResolver.query(voicemailUri(), null, null, null, null);
355            }
356        });
357        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
358            @Override
359            public void run() {
360                mResolver.delete(voicemailUri(), null, null);
361            }
362        });
363    }
364
365    // Test to check that none of the call_log provider specific fields are
366    // insertable through voicemail provider.
367    public void testCannotAccessCallLogSpecificFields_Insert() {
368        for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
369            final ContentValues values = getTestVoicemailValues();
370            values.put(callLogColumn, "foo");
371            EvenMoreAsserts.assertThrows("Column: " + callLogColumn,
372                    IllegalArgumentException.class, new Runnable() {
373                    @Override
374                    public void run() {
375                        mResolver.insert(voicemailUri(), values);
376                    }
377                });
378        }
379    }
380
381    // Test to check that none of the call_log provider specific fields are
382    // exposed through voicemail provider query.
383    public void testCannotAccessCallLogSpecificFields_Query() {
384        // Query.
385        Cursor cursor = mResolver.query(voicemailUri(), null, null, null, null);
386        List<String> columnNames = Arrays.asList(cursor.getColumnNames());
387        assertEquals(NUM_VOICEMAIL_FIELDS, columnNames.size());
388        // None of the call_log provider specific columns should be present.
389        for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
390            assertFalse("Unexpected column: '" + callLogColumn + "' returned.",
391                    columnNames.contains(callLogColumn));
392        }
393    }
394
395    // Test to check that none of the call_log provider specific fields are
396    // updatable through voicemail provider.
397    public void testCannotAccessCallLogSpecificFields_Update() {
398        for (String callLogColumn : CALLLOG_PROVIDER_SPECIFIC_COLUMNS) {
399            final Uri insertedUri = insertVoicemail();
400            final ContentValues values = getTestVoicemailValues();
401            values.put(callLogColumn, "foo");
402            EvenMoreAsserts.assertThrows("Column: " + callLogColumn,
403                    IllegalArgumentException.class, new Runnable() {
404                    @Override
405                    public void run() {
406                        mResolver.update(insertedUri, values, null, null);
407                    }
408                });
409        }
410    }
411
412    // Tests for voicemail status table.
413
414    public void testStatusInsert() throws Exception {
415        ContentValues values = getTestStatusValues();
416        Uri uri = mResolver.insert(statusUri(), values);
417        assertStoredValues(uri, values);
418        assertSelection(uri, values, Status._ID, ContentUris.parseId(uri));
419    }
420
421    // Test to ensure that duplicate entries for the same package still end up as the same record.
422    public void testStatusInsertDuplicate() throws Exception {
423        setUpForFullPermission();
424        ContentValues values = getTestStatusValues();
425        assertNotNull(mResolver.insert(statusUri(), values));
426        assertEquals(1, getCount(statusUri(), null, null));
427
428        // Insertion request for the same package should fail with no change in count.
429        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
430        assertNull(mResolver.insert(statusUri(), values));
431        assertEquals(1, getCount(statusUri(), null, null));
432
433        // Now insert entry for another source package, and it should end up as a separate record.
434        values.put(Status.SOURCE_PACKAGE, "another.package");
435        assertNotNull(mResolver.insert(statusUri(), values));
436        assertEquals(2, getCount(statusUri(), null, null));
437    }
438
439    public void testStatusUpdate() throws Exception {
440        Uri uri = insertTestStatusEntry();
441        ContentValues values = getTestStatusValues();
442        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_NO_CONNECTION);
443        values.put(Status.NOTIFICATION_CHANNEL_STATE,
444                Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING);
445        int count = mResolver.update(uri, values, null, null);
446        assertEquals(1, count);
447        assertStoredValues(uri, values);
448    }
449
450    public void testStatusDelete() {
451        Uri uri = insertTestStatusEntry();
452        int count = mResolver.delete(statusUri(), Status._ID + "="
453                + ContentUris.parseId(uri), null);
454        assertEquals(1, count);
455        assertEquals(0, getCount(uri, null, null));
456    }
457
458    public void testStatusGetType() throws Exception {
459        // Item URI.
460        Uri uri = insertTestStatusEntry();
461        assertEquals(Status.ITEM_TYPE, mResolver.getType(uri));
462
463        // base URIs.
464        assertEquals(Status.DIR_TYPE, mResolver.getType(Status.CONTENT_URI));
465        assertEquals(Status.DIR_TYPE, mResolver.getType(Status.buildSourceUri("foo")));
466    }
467
468    // Basic permission checks for the status table.
469    public void testStatusPermissions() throws Exception {
470        final ContentValues values = getTestStatusValues();
471        // Inserting for another package should fail with any of the URIs.
472        values.put(Status.SOURCE_PACKAGE, "another.package");
473        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
474            @Override
475            public void run() {
476                mResolver.insert(Status.CONTENT_URI, values);
477            }
478        });
479        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
480            @Override
481            public void run() {
482                mResolver.insert(Status.buildSourceUri(mActor.packageName), values);
483            }
484        });
485
486        // But insertion with own package should succeed with the right uri.
487        values.put(Status.SOURCE_PACKAGE, mActor.packageName);
488        final Uri uri = mResolver.insert(Status.buildSourceUri(mActor.packageName), values);
489        assertNotNull(uri);
490
491        // Updating source_package should not work as well.
492        values.put(Status.SOURCE_PACKAGE, "another.package");
493        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
494            @Override
495            public void run() {
496                mResolver.update(uri, values, null, null);
497            }
498        });
499    }
500
501    // File operation is not supported by /status URI.
502    public void testStatusFileOperation() throws Exception {
503        final Uri uri = insertTestStatusEntry();
504        EvenMoreAsserts.assertThrows(UnsupportedOperationException.class, new Runnable() {
505            @Override
506            public void run() {
507                try {
508                    mResolver.openOutputStream(uri);
509                } catch (FileNotFoundException e) {
510                    fail("Unexpected exception " + e);
511                }
512            }
513        });
514
515        EvenMoreAsserts.assertThrows(UnsupportedOperationException.class, new Runnable() {
516            @Override
517            public void run() {
518                try {
519                    mResolver.openInputStream(uri);
520                } catch (FileNotFoundException e) {
521                    fail("Unexpected exception " + e);
522                }
523            }
524        });
525    }
526
527    /**
528     * Inserts a voicemail record with no source package set. The content provider
529     * will detect source package.
530     */
531    private Uri insertVoicemail() {
532        return mResolver.insert(voicemailUri(), getTestVoicemailValues());
533    }
534
535    /** Inserts a voicemail record for the specified source package. */
536    private Uri insertVoicemailForSourcePackage(String sourcePackage) {
537        ContentValues values = getTestVoicemailValues();
538        values.put(Voicemails.SOURCE_PACKAGE, sourcePackage);
539        return mResolver.insert(voicemailUri(), values);
540    }
541
542    private ContentValues getTestVoicemailValues() {
543        ContentValues values = new ContentValues();
544        values.put(Voicemails.NUMBER, "1-800-4664-411");
545        values.put(Voicemails.DATE, 1000);
546        values.put(Voicemails.DURATION, 30);
547        values.put(Voicemails.IS_READ, 0);
548        values.put(Voicemails.HAS_CONTENT, 0);
549        values.put(Voicemails.SOURCE_DATA, "1234");
550        values.put(Voicemails.STATE, Voicemails.STATE_INBOX);
551        return values;
552    }
553
554    private Uri insertTestStatusEntry() {
555        return mResolver.insert(statusUri(), getTestStatusValues());
556    }
557
558    private ContentValues getTestStatusValues() {
559        ContentValues values = new ContentValues();
560        values.put(Status.SOURCE_PACKAGE, mActor.packageName);
561        values.put(Status.VOICEMAIL_ACCESS_URI, "tel:901");
562        values.put(Status.SETTINGS_URI, "com.example.voicemail.source.SettingsActivity");
563        values.put(Status.CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
564        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_OK);
565        values.put(Status.NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
566        return values;
567    }
568}
569