1/*
2 * Copyright (C) 2013 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 android.support.v4.content;
18
19import static android.provider.OpenableColumns.DISPLAY_NAME;
20import static android.provider.OpenableColumns.SIZE;
21
22import android.content.ContentResolver;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.Environment;
26import android.support.v4.content.FileProvider.SimplePathStrategy;
27import android.test.AndroidTestCase;
28import android.test.MoreAsserts;
29
30import java.io.File;
31import java.io.FileNotFoundException;
32import java.io.FileOutputStream;
33import java.io.InputStream;
34import java.io.OutputStream;
35
36import libcore.io.IoUtils;
37import libcore.io.Streams;
38
39/**
40 * Tests for {@link FileProvider}
41 */
42public class FileProviderTest extends AndroidTestCase {
43    private static final String TEST_AUTHORITY = "moocow";
44
45    private static final String TEST_FILE = "file.test";
46    private static final byte[] TEST_DATA = new byte[] { (byte) 0xf0, 0x00, 0x0d };
47    private static final byte[] TEST_DATA_ALT = new byte[] { (byte) 0x33, 0x66 };
48
49    private ContentResolver mResolver;
50
51    @Override
52    protected void setUp() throws Exception {
53        super.setUp();
54
55        mResolver = getContext().getContentResolver();
56    }
57
58    public void testStrategyUriSimple() throws Exception {
59        final SimplePathStrategy strat = new SimplePathStrategy("authority");
60        strat.addRoot("tag", mContext.getFilesDir());
61
62        File file = buildPath(mContext.getFilesDir(), "file.test");
63        assertEquals("content://authority/tag/file.test",
64                strat.getUriForFile(file).toString());
65
66        file = buildPath(mContext.getFilesDir(), "subdir", "file.test");
67        assertEquals("content://authority/tag/subdir/file.test",
68                strat.getUriForFile(file).toString());
69
70        file = buildPath(Environment.getExternalStorageDirectory(), "file.test");
71        try {
72            strat.getUriForFile(file);
73            fail("somehow got uri for file outside roots?");
74        } catch (IllegalArgumentException e) {
75        }
76    }
77
78    public void testStrategyUriJumpOutside() throws Exception {
79        final SimplePathStrategy strat = new SimplePathStrategy("authority");
80        strat.addRoot("tag", mContext.getFilesDir());
81
82        File file = buildPath(mContext.getFilesDir(), "..", "file.test");
83        try {
84            strat.getUriForFile(file);
85            fail("file escaped!");
86        } catch (IllegalArgumentException e) {
87        }
88    }
89
90    public void testStrategyUriShortestRoot() throws Exception {
91        SimplePathStrategy strat = new SimplePathStrategy("authority");
92        strat.addRoot("tag1", mContext.getFilesDir());
93        strat.addRoot("tag2", new File("/"));
94
95        File file = buildPath(mContext.getFilesDir(), "file.test");
96        assertEquals("content://authority/tag1/file.test",
97                strat.getUriForFile(file).toString());
98
99        strat = new SimplePathStrategy("authority");
100        strat.addRoot("tag1", new File("/"));
101        strat.addRoot("tag2", mContext.getFilesDir());
102
103        file = buildPath(mContext.getFilesDir(), "file.test");
104        assertEquals("content://authority/tag2/file.test",
105                strat.getUriForFile(file).toString());
106    }
107
108    public void testStrategyFileSimple() throws Exception {
109        final SimplePathStrategy strat = new SimplePathStrategy("authority");
110        strat.addRoot("tag", mContext.getFilesDir());
111
112        File file = buildPath(mContext.getFilesDir(), "file.test");
113        assertEquals(file.getPath(),
114                strat.getFileForUri(Uri.parse("content://authority/tag/file.test")).getPath());
115
116        file = buildPath(mContext.getFilesDir(), "subdir", "file.test");
117        assertEquals(file.getPath(), strat.getFileForUri(
118                Uri.parse("content://authority/tag/subdir/file.test")).getPath());
119    }
120
121    public void testStrategyFileJumpOutside() throws Exception {
122        final SimplePathStrategy strat = new SimplePathStrategy("authority");
123        strat.addRoot("tag", mContext.getFilesDir());
124
125        try {
126            strat.getFileForUri(Uri.parse("content://authority/tag/../file.test"));
127            fail("file escaped!");
128        } catch (SecurityException e) {
129        }
130    }
131
132    public void testStrategyEscaping() throws Exception {
133        final SimplePathStrategy strat = new SimplePathStrategy("authority");
134        strat.addRoot("t/g", mContext.getFilesDir());
135
136        File file = buildPath(mContext.getFilesDir(), "lol\"wat?foo&bar", "wat.txt");
137        final String expected = "content://authority/t%2Fg/lol%22wat%3Ffoo%26bar/wat.txt";
138
139        assertEquals(expected,
140                strat.getUriForFile(file).toString());
141        assertEquals(file.getPath(),
142                strat.getFileForUri(Uri.parse(expected)).getPath());
143    }
144
145    public void testStrategyExtraParams() throws Exception {
146        final SimplePathStrategy strat = new SimplePathStrategy("authority");
147        strat.addRoot("tag", mContext.getFilesDir());
148
149        File file = buildPath(mContext.getFilesDir(), "file.txt");
150        assertEquals(file.getPath(), strat.getFileForUri(
151                Uri.parse("content://authority/tag/file.txt?extra=foo")).getPath());
152    }
153
154    public void testStrategyExtraSeparators() throws Exception {
155        final SimplePathStrategy strat = new SimplePathStrategy("authority");
156        strat.addRoot("tag", mContext.getFilesDir());
157
158        // When canonicalized, the path separators are trimmed
159        File inFile = new File(mContext.getFilesDir(), "//foo//bar//");
160        File outFile = new File(mContext.getFilesDir(), "/foo/bar");
161        final String expected = "content://authority/tag/foo/bar";
162
163        assertEquals(expected,
164                strat.getUriForFile(inFile).toString());
165        assertEquals(outFile.getPath(),
166                strat.getFileForUri(Uri.parse(expected)).getPath());
167    }
168
169    public void testQueryProjectionNull() throws Exception {
170        final File file = new File(mContext.getFilesDir(), TEST_FILE);
171        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
172
173        // Verify that null brings out default columns
174        Cursor cursor = mResolver.query(uri, null, null, null, null);
175        try {
176            assertEquals(1, cursor.getCount());
177            cursor.moveToFirst();
178            assertEquals(TEST_FILE, cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)));
179            assertEquals(TEST_DATA.length, cursor.getLong(cursor.getColumnIndex(SIZE)));
180        } finally {
181            cursor.close();
182        }
183    }
184
185    public void testQueryProjectionOrder() throws Exception {
186        final File file = new File(mContext.getFilesDir(), TEST_FILE);
187        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
188
189        // Verify that swapped order works
190        Cursor cursor = mResolver.query(uri, new String[] {
191                SIZE, DISPLAY_NAME }, null, null, null);
192        try {
193            assertEquals(1, cursor.getCount());
194            cursor.moveToFirst();
195            assertEquals(TEST_DATA.length, cursor.getLong(0));
196            assertEquals(TEST_FILE, cursor.getString(1));
197        } finally {
198            cursor.close();
199        }
200
201        cursor = mResolver.query(uri, new String[] {
202                DISPLAY_NAME, SIZE }, null, null, null);
203        try {
204            assertEquals(1, cursor.getCount());
205            cursor.moveToFirst();
206            assertEquals(TEST_FILE, cursor.getString(0));
207            assertEquals(TEST_DATA.length, cursor.getLong(1));
208        } finally {
209            cursor.close();
210        }
211    }
212
213    public void testQueryExtraColumn() throws Exception {
214        final File file = new File(mContext.getFilesDir(), TEST_FILE);
215        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
216
217        // Verify that extra column doesn't gook things up
218        Cursor cursor = mResolver.query(uri, new String[] {
219                SIZE, "foobar", DISPLAY_NAME }, null, null, null);
220        try {
221            assertEquals(1, cursor.getCount());
222            cursor.moveToFirst();
223            assertEquals(TEST_DATA.length, cursor.getLong(0));
224            assertEquals(TEST_FILE, cursor.getString(1));
225        } finally {
226            cursor.close();
227        }
228    }
229
230    public void testReadFile() throws Exception {
231        final File file = new File(mContext.getFilesDir(), TEST_FILE);
232        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
233
234        assertContentsEquals(TEST_DATA, uri);
235    }
236
237    public void testWriteFile() throws Exception {
238        final File file = new File(mContext.getFilesDir(), TEST_FILE);
239        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
240
241        assertContentsEquals(TEST_DATA, uri);
242
243        final OutputStream out = mResolver.openOutputStream(uri);
244        try {
245            out.write(TEST_DATA_ALT);
246        } finally {
247            IoUtils.closeQuietly(out);
248        }
249
250        assertContentsEquals(TEST_DATA_ALT, uri);
251    }
252
253    public void testWriteMissingFile() throws Exception {
254        final File file = new File(mContext.getFilesDir(), TEST_FILE);
255        final Uri uri = stageFileAndGetUri(file, null);
256
257        try {
258            assertContentsEquals(new byte[0], uri);
259            fail("Somehow read missing file?");
260        } catch(FileNotFoundException e) {
261        }
262
263        final OutputStream out = mResolver.openOutputStream(uri);
264        try {
265            out.write(TEST_DATA_ALT);
266        } finally {
267            IoUtils.closeQuietly(out);
268        }
269
270        assertContentsEquals(TEST_DATA_ALT, uri);
271    }
272
273    public void testDelete() throws Exception {
274        final File file = new File(mContext.getFilesDir(), TEST_FILE);
275        final Uri uri = stageFileAndGetUri(file, TEST_DATA);
276
277        assertContentsEquals(TEST_DATA, uri);
278
279        assertEquals(1, mResolver.delete(uri, null, null));
280        assertEquals(0, mResolver.delete(uri, null, null));
281
282        try {
283            assertContentsEquals(new byte[0], uri);
284            fail("Somehow read missing file?");
285        } catch(FileNotFoundException e) {
286        }
287    }
288
289    public void testMetaDataTargets() {
290        Uri actual;
291
292        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
293                new File("/proc/version"));
294        assertEquals("content://moocow/test_root/proc/version", actual.toString());
295
296        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
297                new File("/proc/1/mountinfo"));
298        assertEquals("content://moocow/test_init/mountinfo", actual.toString());
299
300        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
301                buildPath(mContext.getFilesDir(), "meow"));
302        assertEquals("content://moocow/test_files/meow", actual.toString());
303
304        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
305                buildPath(mContext.getFilesDir(), "thumbs", "rawr"));
306        assertEquals("content://moocow/test_thumbs/rawr", actual.toString());
307
308        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
309                buildPath(mContext.getCacheDir(), "up", "down"));
310        assertEquals("content://moocow/test_cache/up/down", actual.toString());
311
312        actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY,
313                buildPath(Environment.getExternalStorageDirectory(), "Android", "obb", "foobar"));
314        assertEquals("content://moocow/test_external/Android/obb/foobar", actual.toString());
315    }
316
317    private void assertContentsEquals(byte[] expected, Uri actual) throws Exception {
318        final InputStream in = mResolver.openInputStream(actual);
319        try {
320            MoreAsserts.assertEquals(expected, Streams.readFully(in));
321        } finally {
322            IoUtils.closeQuietly(in);
323        }
324    }
325
326    private Uri stageFileAndGetUri(File file, byte[] data) throws Exception {
327        if (data != null) {
328            final FileOutputStream out = new FileOutputStream(file);
329            try {
330                out.write(data);
331            } finally {
332                out.close();
333            }
334        } else {
335            file.delete();
336        }
337        return FileProvider.getUriForFile(mContext, TEST_AUTHORITY, file);
338    }
339
340    private static File buildPath(File base, String... segments) {
341        File cur = base;
342        for (String segment : segments) {
343            if (cur == null) {
344                cur = new File(segment);
345            } else {
346                cur = new File(cur, segment);
347            }
348        }
349        return cur;
350    }
351}
352