1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package android.content.cts;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNull;
25 import static org.junit.Assert.assertSame;
26 import static org.junit.Assert.assertTrue;
27 import static org.junit.Assert.fail;
28 import static org.testng.Assert.assertThrows;
29 
30 import android.content.ContentProvider;
31 import android.content.ContentProvider.CallingIdentity;
32 import android.content.ContentResolver;
33 import android.content.ContentValues;
34 import android.content.Context;
35 import android.content.pm.PackageManager;
36 import android.content.pm.ProviderInfo;
37 import android.content.res.AssetFileDescriptor;
38 import android.database.Cursor;
39 import android.database.sqlite.SQLiteDatabase;
40 import android.net.Uri;
41 import android.os.Binder;
42 import android.os.Bundle;
43 import android.os.ParcelFileDescriptor;
44 import android.os.UserHandle;
45 import android.platform.test.annotations.AppModeFull;
46 import android.platform.test.annotations.AppModeSdkSandbox;
47 import android.provider.MediaStore;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 import androidx.test.core.app.ApplicationProvider;
52 
53 import com.android.bedstead.harrier.BedsteadJUnit4;
54 import com.android.bedstead.harrier.DeviceState;
55 import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile;
56 import com.android.bedstead.nene.TestApis;
57 
58 import org.junit.After;
59 import org.junit.ClassRule;
60 import org.junit.Rule;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 
64 import java.io.DataInputStream;
65 import java.io.DataOutputStream;
66 import java.io.File;
67 import java.io.FileInputStream;
68 import java.io.FileNotFoundException;
69 import java.io.FileOutputStream;
70 import java.io.IOException;
71 import java.io.InputStream;
72 
73 /**
74  * Test {@link ContentProvider}.
75  */
76 @RunWith(BedsteadJUnit4.class)
77 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
78 public class ContentProviderTest {
79     private static final String TEST_PACKAGE_NAME = "android.content.cts";
80     private static final String TEST_FILE_NAME = "testFile.tmp";
81     private static final String TEST_DB_NAME = "test.db";
82 
83     @ClassRule
84     @Rule
85     public static final DeviceState sDeviceState = new DeviceState();
86 
87     private static final Context sContext = ApplicationProvider.getApplicationContext();
88     @After
tearDown()89     public void tearDown() throws Exception {
90         sContext.deleteDatabase(TEST_DB_NAME);
91         sContext.deleteFile(TEST_FILE_NAME);
92     }
93 
94     @Test
testOpenAssetFile()95     public void testOpenAssetFile() throws IOException {
96         MockContentProvider mockContentProvider = new MockContentProvider();
97         Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE +
98                 "://" + TEST_PACKAGE_NAME + "/" + R.raw.testimage);
99 
100         try {
101             mockContentProvider.openAssetFile(uri, "r");
102             fail("Should always throw out FileNotFoundException!");
103         } catch (FileNotFoundException e) {
104         }
105 
106         try {
107             mockContentProvider.openFile(null, null);
108             fail("Should always throw out FileNotFoundException!");
109         } catch (FileNotFoundException e) {
110         }
111     }
112 
113     @Test
testAttachInfo()114     public void testAttachInfo() {
115         MockContentProvider mockContentProvider = new MockContentProvider();
116 
117         ProviderInfo info1 = new ProviderInfo();
118         info1.readPermission = "android.permission.READ_SMS";
119         info1.writePermission = null; // Guarded by an app op not a permission.
120         mockContentProvider.attachInfo(sContext, info1);
121         assertSame(sContext, mockContentProvider.getContext());
122         assertEquals(info1.readPermission, mockContentProvider.getReadPermission());
123         assertEquals(info1.writePermission, mockContentProvider.getWritePermission());
124 
125         ProviderInfo info2 = new ProviderInfo();
126         info2.readPermission = "android.permission.READ_CONTACTS";
127         info2.writePermission = "android.permission.WRITE_CONTACTS";
128         mockContentProvider.attachInfo(null, info2);
129         assertSame(sContext, mockContentProvider.getContext());
130         assertEquals(info1.readPermission, mockContentProvider.getReadPermission());
131         assertEquals(info1.writePermission, mockContentProvider.getWritePermission());
132 
133         mockContentProvider = new MockContentProvider();
134         mockContentProvider.attachInfo(null, null);
135         assertNull(mockContentProvider.getContext());
136         assertNull(mockContentProvider.getReadPermission());
137         assertNull(mockContentProvider.getWritePermission());
138 
139         mockContentProvider.attachInfo(null, info2);
140         assertNull(mockContentProvider.getContext());
141         assertEquals(info2.readPermission, mockContentProvider.getReadPermission());
142         assertEquals(info2.writePermission, mockContentProvider.getWritePermission());
143 
144         mockContentProvider.attachInfo(sContext, info1);
145         assertSame(sContext, mockContentProvider.getContext());
146         assertEquals(info1.readPermission, mockContentProvider.getReadPermission());
147         assertEquals(info1.writePermission, mockContentProvider.getWritePermission());
148     }
149 
150     @Test
testBulkInsert()151     public void testBulkInsert() {
152         MockContentProvider mockContentProvider = new MockContentProvider();
153 
154         int count = 2;
155         ContentValues[] values = new ContentValues[count];
156         for (int i = 0; i < count; i++) {
157             values[i] = new ContentValues();
158         }
159         Uri uri = Uri.parse("content://browser/bookmarks");
160         assertEquals(count, mockContentProvider.bulkInsert(uri, values));
161         assertEquals(count, mockContentProvider.getInsertCount());
162 
163         mockContentProvider = new MockContentProvider();
164         try {
165             assertEquals(count, mockContentProvider.bulkInsert(null, values));
166         } finally {
167             assertEquals(count, mockContentProvider.getInsertCount());
168         }
169     }
170 
171     @Test
testGetContext()172     public void testGetContext() {
173         MockContentProvider mockContentProvider = new MockContentProvider();
174         assertNull(mockContentProvider.getContext());
175 
176         mockContentProvider.attachInfo(sContext, null);
177         assertSame(sContext, mockContentProvider.getContext());
178         mockContentProvider.attachInfo(null, null);
179         assertSame(sContext, mockContentProvider.getContext());
180     }
181 
182     @Test
testAccessReadPermission()183     public void testAccessReadPermission() {
184         MockContentProvider mockContentProvider = new MockContentProvider();
185         assertNull(mockContentProvider.getReadPermission());
186 
187         String expected = "android.permission.READ_CONTACTS";
188         mockContentProvider.setReadPermissionWrapper(expected);
189         assertEquals(expected, mockContentProvider.getReadPermission());
190 
191         expected = "android.permission.READ_SMS";
192         mockContentProvider.setReadPermissionWrapper(expected);
193         assertEquals(expected, mockContentProvider.getReadPermission());
194 
195         mockContentProvider.setReadPermissionWrapper(null);
196         assertNull(mockContentProvider.getReadPermission());
197     }
198 
199     @Test
testAccessWritePermission()200     public void testAccessWritePermission() {
201         MockContentProvider mockContentProvider = new MockContentProvider();
202         assertNull(mockContentProvider.getWritePermission());
203 
204         String expected = "android.permission.WRITE_CONTACTS";
205         mockContentProvider.setWritePermissionWrapper(expected);
206         assertEquals(expected, mockContentProvider.getWritePermission());
207 
208         mockContentProvider.setWritePermissionWrapper(null);
209         assertNull(mockContentProvider.getWritePermission());
210     }
211 
212     @Test
testIsTemporary()213     public void testIsTemporary() {
214         MockContentProvider mockContentProvider = new MockContentProvider();
215         assertFalse(mockContentProvider.isTemporary());
216     }
217 
218     @Test
testOpenFile()219     public void testOpenFile() {
220         MockContentProvider mockContentProvider = new MockContentProvider();
221 
222         try {
223             Uri uri = Uri.parse("content://test");
224             mockContentProvider.openFile(uri, "r");
225             fail("Should always throw out FileNotFoundException!");
226         } catch (FileNotFoundException e) {
227         }
228 
229         try {
230             mockContentProvider.openFile(null, null);
231             fail("Should always throw out FileNotFoundException!");
232         } catch (FileNotFoundException e) {
233         }
234     }
235 
236     @Test
testOpenFileHelper()237     public void testOpenFileHelper() throws IOException {
238         // create a temporary File
239         sContext.openFileOutput(TEST_FILE_NAME, Context.MODE_PRIVATE).close();
240         File file = sContext.getFileStreamPath(TEST_FILE_NAME);
241         assertTrue(file.exists());
242 
243         ContentProvider cp = new OpenFilePipeContentProvider(file.getAbsolutePath(), TEST_DB_NAME);
244 
245         Uri uri = Uri.parse("content://test");
246         assertNotNull(cp.openFile(uri, "r"));
247 
248         try {
249             uri = Uri.parse("content://test");
250             cp.openFile(uri, "wrong");
251             fail("Should throw IllegalArgumentException for bad mode!");
252         } catch (IllegalArgumentException e) {
253         }
254 
255         // delete the temporary file
256         file.delete();
257 
258         try {
259             uri = Uri.parse("content://test");
260             cp.openFile(uri, "r");
261             fail("Should throw FileNotFoundException!");
262         } catch (FileNotFoundException e) {
263         }
264 
265         try {
266             cp.openFile((Uri) null, "r");
267             fail("Should always throw FileNotFoundException!");
268         } catch (FileNotFoundException e) {
269         }
270     }
271 
assertAssetFileContents(AssetFileDescriptor assetFileDescriptor, String message)272     private static void assertAssetFileContents(AssetFileDescriptor assetFileDescriptor,
273             String message) throws IOException {
274         assertNotNull(assetFileDescriptor);
275         try (DataInputStream dis = new DataInputStream(
276                 new FileInputStream(assetFileDescriptor.getFileDescriptor()))) {
277             assertEquals(message, dis.readUTF());
278         }
279     }
280 
281     @Test
testOpenPipeHelper()282     public void testOpenPipeHelper() throws IOException {
283         // create a temporary File
284         sContext.openFileOutput(TEST_FILE_NAME, Context.MODE_PRIVATE).close();
285         File file = sContext.getFileStreamPath(TEST_FILE_NAME);
286         assertTrue(file.exists());
287 
288         ContentProvider cp = new OpenFilePipeContentProvider(file.getAbsolutePath(), TEST_DB_NAME);
289 
290         Uri uri = Uri.parse("content://test");
291         assertAssetFileContents(cp.openAssetFile(uri, "r"), "OK");
292 
293         uri = Uri.parse("content://test");
294         assertAssetFileContents(cp.openAssetFile(uri, "wrong"),
295                 "java.lang.IllegalArgumentException: Bad mode: wrong");
296 
297         // delete the temporary file
298         file.delete();
299 
300         uri = Uri.parse("content://test");
301         assertAssetFileContents(cp.openAssetFile(uri, "r"),
302                 "java.io.FileNotFoundException: open failed: ENOENT (No such file or directory)");
303 
304         assertAssetFileContents(cp.openAssetFile((Uri) null, "r"),
305                 "java.io.FileNotFoundException: open failed: ENOENT (No such file or directory)");
306     }
307 
308     @Test
testOnConfigurationChanged()309     public void testOnConfigurationChanged() {
310         // cannot trigger this callback reliably
311     }
312 
313     @Test
testOnLowMemory()314     public void testOnLowMemory() {
315         // cannot trigger this callback reliably
316     }
317 
318     @Test
testRefresh_DefaultImplReturnsFalse()319     public void testRefresh_DefaultImplReturnsFalse() {
320         MockContentProvider provider = new MockContentProvider();
321         assertFalse(provider.refresh(null, null, null));
322     }
323 
324     @Test
testGetIContentProvider()325     public void testGetIContentProvider() {
326         MockContentProvider mockContentProvider = new MockContentProvider();
327 
328         assertNotNull(mockContentProvider.getIContentProvider());
329     }
330 
331     @Test
testClearCallingIdentity()332     public void testClearCallingIdentity() {
333         final MockContentProvider provider = new MockContentProvider();
334         provider.attachInfo(sContext, new ProviderInfo());
335 
336         final CallingIdentity ident = provider.clearCallingIdentity();
337         try {
338             assertEquals(android.os.Process.myUid(), Binder.getCallingUid());
339             assertEquals(null, provider.getCallingPackage());
340         } finally {
341             provider.restoreCallingIdentity(ident);
342         }
343     }
344 
345     @Test
testCheckUriPermission()346     public void testCheckUriPermission() {
347         MockContentProvider provider = new MockContentProvider();
348         final Uri uri = Uri.parse("content://test");
349         assertEquals(PackageManager.PERMISSION_DENIED,
350                 provider.checkUriPermission(uri, android.os.Process.myUid(), 0));
351     }
352 
353     @Test
testCreateContentUriForUser_nullUri_throwsNPE()354     public void testCreateContentUriForUser_nullUri_throwsNPE() {
355         assertThrows(
356                 NullPointerException.class,
357                 () -> ContentProvider.createContentUriForUser(null, UserHandle.of(7)));
358     }
359 
360     @Test
testCreateContentUriForUser_nonContentUri_throwsIAE()361     public void testCreateContentUriForUser_nonContentUri_throwsIAE() {
362         final Uri uri = Uri.parse("notcontent://test");
363         assertThrows(
364                 IllegalArgumentException.class,
365                 () -> ContentProvider.createContentUriForUser(uri, UserHandle.of(7)));
366     }
367 
368     @Test
testCreateContentUriForUser_UriWithDifferentUserID_throwsIAE()369     public void testCreateContentUriForUser_UriWithDifferentUserID_throwsIAE() {
370         final Uri uri = Uri.parse("content://07@Test");
371         assertThrows(
372                 IllegalArgumentException.class,
373                 () -> ContentProvider.createContentUriForUser(uri, UserHandle.of(7)));
374     }
375 
376     @Test
testCreateContentUriForUser_UriWithUserID_unchanged()377     public void testCreateContentUriForUser_UriWithUserID_unchanged() {
378         final Uri uri = Uri.parse("content://7@Test");
379         assertEquals(uri, ContentProvider.createContentUriForUser(uri, UserHandle.of(7)));
380     }
381 
382     @Test
383     @EnsureHasWorkProfile
384     @AppModeFull
createContentUriForUser_returnsCorrectUri()385     public void createContentUriForUser_returnsCorrectUri() {
386         final ContentResolver profileContentResolver =
387                 TestApis.context().androidContextAsUser(sDeviceState.workProfile())
388                         .getContentResolver();
389         final String testContentDisplayName = "testContent.mp3";
390         final Uri workProfileUriWithoutUserId = createAndInsertTestAudioFile(
391                 profileContentResolver, testContentDisplayName);
392 
393         final Uri workProfileUriWithUserId = ContentProvider.createContentUriForUser(
394                 workProfileUriWithoutUserId, sDeviceState.workProfile().userHandle());
395 
396         assertThat(getAudioContentDisplayName(
397                 sContext.getContentResolver(), workProfileUriWithUserId))
398                 .isEqualTo(testContentDisplayName);
399     }
400 
createAndInsertTestAudioFile(ContentResolver resolver, String displayName)401     private Uri createAndInsertTestAudioFile(ContentResolver resolver, String displayName) {
402         final Uri audioCollection = MediaStore.Audio.Media.getContentUri(
403                 MediaStore.VOLUME_EXTERNAL_PRIMARY);
404         final ContentValues testContent = new ContentValues();
405         testContent.put(MediaStore.Audio.Media.DISPLAY_NAME, displayName);
406         return resolver.insert(audioCollection, testContent);
407     }
408 
getAudioContentDisplayName(ContentResolver resolver, Uri uri)409     private String getAudioContentDisplayName(ContentResolver resolver, Uri uri) {
410         String name = null;
411         try (Cursor cursor = resolver.query(
412                 uri,
413                 /* projection = */ null,
414                 /* selection = */ null,
415                 /* selectionArgs = */ null,
416                 /* sortOrder = */ null)) {
417             final int nameColumn =
418                     cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME);
419             if (cursor.moveToNext()) {
420                 name = cursor.getString(nameColumn);
421             }
422         }
423         return name;
424     }
425 
426     private class MockContentProvider extends ContentProvider {
427         private int mInsertCount = 0;
428 
429         @Override
delete(Uri uri, String selection, String[] selectionArgs)430         public int delete(Uri uri, String selection, String[] selectionArgs) {
431             return 0;
432         }
433 
434         @Override
getType(Uri uri)435         public String getType(Uri uri) {
436             return null;
437         }
438 
439         @Override
insert(Uri uri, ContentValues values)440         public Uri insert(Uri uri, ContentValues values) {
441             mInsertCount++;
442             return null;
443         }
444 
getInsertCount()445         public int getInsertCount() {
446             return mInsertCount;
447         }
448 
449         @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)450         public Cursor query(Uri uri, String[] projection, String selection,
451                 String[] selectionArgs, String sortOrder) {
452             return null;
453         }
454 
455         @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)456         public int update(Uri uri, ContentValues values, String selection,
457                 String[] selectionArgs) {
458             return 0;
459         }
460 
461         @Override
onCreate()462         public boolean onCreate() {
463             return false;
464         }
465 
466         // wrapper or override for the protected methods
setReadPermissionWrapper(String permission)467         public void setReadPermissionWrapper(String permission) {
468             super.setReadPermission(permission);
469         }
470 
setWritePermissionWrapper(String permission)471         public void setWritePermissionWrapper(String permission) {
472             super.setWritePermission(permission);
473         }
474 
475         @Override
isTemporary()476         protected boolean isTemporary() {
477             return super.isTemporary();
478         }
479 
openFileHelperWrapper(Uri uri, String mode)480         public ParcelFileDescriptor openFileHelperWrapper(Uri uri, String mode)
481                 throws FileNotFoundException {
482             return super.openFileHelper(uri, mode);
483         }
484     }
485 
486     /**
487      * This provider implements openFile/openAssetFile() using
488      * ContentProvider.openFileHelper/openPipeHelper().
489      */
490     private class OpenFilePipeContentProvider extends ContentProvider implements
491             ContentProvider.PipeDataWriter<String> {
492         private SQLiteDatabase mDb;
493 
OpenFilePipeContentProvider(String fileName, String dbName)494         OpenFilePipeContentProvider(String fileName, String dbName) {
495             // delete the database if it already exists
496             sContext.deleteDatabase(dbName);
497             mDb = sContext.openOrCreateDatabase(dbName, Context.MODE_PRIVATE, null);
498             mDb.execSQL("CREATE TABLE files ( _data TEXT );");
499             mDb.execSQL("INSERT INTO files VALUES ( \"" + fileName + "\");");
500         }
501 
502         @Override
delete(Uri uri, String selection, String[] selectionArgs)503         public int delete(Uri uri, String selection, String[] selectionArgs) {
504             throw new RuntimeException("not implemented");
505         }
506 
507         @Override
getType(Uri uri)508         public String getType(Uri uri) {
509             throw new RuntimeException("not implemented");
510         }
511 
512         @Override
insert(Uri uri, ContentValues values)513         public Uri insert(Uri uri, ContentValues values) {
514             throw new RuntimeException("not implemented");
515         }
516 
517         @Override
onCreate()518         public boolean onCreate() {
519             return true;
520         }
521 
522         @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)523         public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
524                 String sortOrder) {
525             return mDb.query("files", projection, selection, selectionArgs, null, null, null);
526         }
527 
528         @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)529         public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
530             throw new RuntimeException("not implemented");
531         }
532 
533         @Override
openFile(Uri uri, String mode)534         public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
535             return openFileHelper(uri, mode);
536         }
537 
538         @Override
openAssetFile(Uri uri, String mode)539         public AssetFileDescriptor openAssetFile(Uri uri, String mode)
540                 throws FileNotFoundException {
541             return new AssetFileDescriptor(openPipeHelper(uri, "text/html", null,
542                     mode, this), 0,
543                     AssetFileDescriptor.UNKNOWN_LENGTH);
544         }
545 
546         @Override
writeDataToPipe(@onNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args)547         public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri,
548                 @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) {
549             try (DataOutputStream dos = new DataOutputStream(
550                     new FileOutputStream(output.getFileDescriptor()))) {
551                 try (InputStream is = new FileInputStream(
552                         openFile(uri, args).getFileDescriptor())) {
553                     dos.writeUTF("OK");
554                 } catch (Throwable t) {
555                     dos.writeUTF(t.toString());
556                 }
557             } catch (IOException ignored) {
558 
559             }
560         }
561     }
562 }
563