| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.core.content; |
| |
| import static android.provider.OpenableColumns.DISPLAY_NAME; |
| import static android.provider.OpenableColumns.SIZE; |
| |
| import static org.junit.Assert.assertArrayEquals; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.fail; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Environment; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.SmallTest; |
| import android.support.test.runner.AndroidJUnit4; |
| |
| import androidx.core.content.FileProvider.SimplePathStrategy; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| |
| /** |
| * Tests for {@link FileProvider} |
| */ |
| @SmallTest |
| @RunWith(AndroidJUnit4.class) |
| public class FileProviderTest { |
| private static final String TEST_AUTHORITY = "moocow"; |
| |
| private static final String TEST_FILE = "file.test"; |
| private static final byte[] TEST_DATA = new byte[] { (byte) 0xf0, 0x00, 0x0d }; |
| private static final byte[] TEST_DATA_ALT = new byte[] { (byte) 0x33, 0x66 }; |
| |
| private ContentResolver mResolver; |
| private Context mContext; |
| |
| @Before |
| public void setup() throws Exception { |
| mContext = InstrumentationRegistry.getTargetContext(); |
| mResolver = mContext.getContentResolver(); |
| } |
| |
| @Test |
| public void testStrategyUriSimple() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag", mContext.getFilesDir()); |
| |
| File file = buildPath(mContext.getFilesDir(), "file.test"); |
| assertEquals("content://authority/tag/file.test", |
| strat.getUriForFile(file).toString()); |
| |
| file = buildPath(mContext.getFilesDir(), "subdir", "file.test"); |
| assertEquals("content://authority/tag/subdir/file.test", |
| strat.getUriForFile(file).toString()); |
| |
| file = buildPath(Environment.getExternalStorageDirectory(), "file.test"); |
| try { |
| strat.getUriForFile(file); |
| fail("somehow got uri for file outside roots?"); |
| } catch (IllegalArgumentException e) { |
| } |
| } |
| |
| @Test |
| public void testStrategyUriJumpOutside() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag", mContext.getFilesDir()); |
| |
| File file = buildPath(mContext.getFilesDir(), "..", "file.test"); |
| try { |
| strat.getUriForFile(file); |
| fail("file escaped!"); |
| } catch (IllegalArgumentException e) { |
| } |
| } |
| |
| @Test |
| public void testStrategyUriShortestRoot() throws Exception { |
| SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag1", mContext.getFilesDir()); |
| strat.addRoot("tag2", new File("/")); |
| |
| File file = buildPath(mContext.getFilesDir(), "file.test"); |
| assertEquals("content://authority/tag1/file.test", |
| strat.getUriForFile(file).toString()); |
| |
| strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag1", new File("/")); |
| strat.addRoot("tag2", mContext.getFilesDir()); |
| |
| file = buildPath(mContext.getFilesDir(), "file.test"); |
| assertEquals("content://authority/tag2/file.test", |
| strat.getUriForFile(file).toString()); |
| } |
| |
| @Test |
| public void testStrategyFileSimple() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag", mContext.getFilesDir()); |
| |
| File expectedRoot = mContext.getFilesDir().getCanonicalFile(); |
| File file = buildPath(expectedRoot, "file.test"); |
| assertEquals(file.getPath(), |
| strat.getFileForUri(Uri.parse("content://authority/tag/file.test")).getPath()); |
| |
| file = buildPath(expectedRoot, "subdir", "file.test"); |
| assertEquals(file.getPath(), strat.getFileForUri( |
| Uri.parse("content://authority/tag/subdir/file.test")).getPath()); |
| } |
| |
| @Test |
| public void testStrategyFileJumpOutside() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag", mContext.getFilesDir()); |
| |
| try { |
| strat.getFileForUri(Uri.parse("content://authority/tag/../file.test")); |
| fail("file escaped!"); |
| } catch (SecurityException e) { |
| } |
| } |
| |
| @Test |
| public void testStrategyEscaping() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("t/g", mContext.getFilesDir()); |
| |
| File expectedRoot = mContext.getFilesDir().getCanonicalFile(); |
| File file = buildPath(expectedRoot, "lol\"wat?foo&bar", "wat.txt"); |
| final String expected = "content://authority/t%2Fg/lol%22wat%3Ffoo%26bar/wat.txt"; |
| |
| assertEquals(expected, |
| strat.getUriForFile(file).toString()); |
| assertEquals(file.getPath(), |
| strat.getFileForUri(Uri.parse(expected)).getPath()); |
| } |
| |
| @Test |
| public void testStrategyExtraParams() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag", mContext.getFilesDir()); |
| |
| File expectedRoot = mContext.getFilesDir().getCanonicalFile(); |
| File file = buildPath(expectedRoot, "file.txt"); |
| assertEquals(file.getPath(), strat.getFileForUri( |
| Uri.parse("content://authority/tag/file.txt?extra=foo")).getPath()); |
| } |
| |
| @Test |
| public void testStrategyExtraSeparators() throws Exception { |
| final SimplePathStrategy strat = new SimplePathStrategy("authority"); |
| strat.addRoot("tag", mContext.getFilesDir()); |
| |
| // When canonicalized, the path separators are trimmed |
| File inFile = new File(mContext.getFilesDir(), "//foo//bar//"); |
| File expectedRoot = mContext.getFilesDir().getCanonicalFile(); |
| File outFile = new File(expectedRoot, "/foo/bar"); |
| final String expected = "content://authority/tag/foo/bar"; |
| |
| assertEquals(expected, |
| strat.getUriForFile(inFile).toString()); |
| assertEquals(outFile.getPath(), |
| strat.getFileForUri(Uri.parse(expected)).getPath()); |
| } |
| |
| @Test |
| public void testQueryProjectionNull() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, TEST_DATA); |
| |
| // Verify that null brings out default columns |
| Cursor cursor = mResolver.query(uri, null, null, null, null); |
| try { |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(TEST_FILE, cursor.getString(cursor.getColumnIndex(DISPLAY_NAME))); |
| assertEquals(TEST_DATA.length, cursor.getLong(cursor.getColumnIndex(SIZE))); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| @Test |
| public void testQueryProjectionOrder() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, TEST_DATA); |
| |
| // Verify that swapped order works |
| Cursor cursor = mResolver.query(uri, new String[] { |
| SIZE, DISPLAY_NAME }, null, null, null); |
| try { |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(TEST_DATA.length, cursor.getLong(0)); |
| assertEquals(TEST_FILE, cursor.getString(1)); |
| } finally { |
| cursor.close(); |
| } |
| |
| cursor = mResolver.query(uri, new String[] { |
| DISPLAY_NAME, SIZE }, null, null, null); |
| try { |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(TEST_FILE, cursor.getString(0)); |
| assertEquals(TEST_DATA.length, cursor.getLong(1)); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| @Test |
| public void testQueryExtraColumn() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, TEST_DATA); |
| |
| // Verify that extra column doesn't gook things up |
| Cursor cursor = mResolver.query(uri, new String[] { |
| SIZE, "foobar", DISPLAY_NAME }, null, null, null); |
| try { |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(TEST_DATA.length, cursor.getLong(0)); |
| assertEquals(TEST_FILE, cursor.getString(1)); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| @Test |
| public void testReadFile() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, TEST_DATA); |
| |
| assertContentsEquals(TEST_DATA, uri); |
| } |
| |
| @Test |
| public void testWriteFile() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, TEST_DATA); |
| |
| assertContentsEquals(TEST_DATA, uri); |
| |
| final OutputStream out = mResolver.openOutputStream(uri); |
| try { |
| out.write(TEST_DATA_ALT); |
| } finally { |
| closeQuietly(out); |
| } |
| |
| assertContentsEquals(TEST_DATA_ALT, uri); |
| } |
| |
| @Test |
| public void testWriteMissingFile() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, null); |
| |
| try { |
| assertContentsEquals(new byte[0], uri); |
| fail("Somehow read missing file?"); |
| } catch(FileNotFoundException e) { |
| } |
| |
| final OutputStream out = mResolver.openOutputStream(uri); |
| try { |
| out.write(TEST_DATA_ALT); |
| } finally { |
| closeQuietly(out); |
| } |
| |
| assertContentsEquals(TEST_DATA_ALT, uri); |
| } |
| |
| @Test |
| public void testDelete() throws Exception { |
| final File file = new File(mContext.getFilesDir(), TEST_FILE); |
| final Uri uri = stageFileAndGetUri(file, TEST_DATA); |
| |
| assertContentsEquals(TEST_DATA, uri); |
| |
| assertEquals(1, mResolver.delete(uri, null, null)); |
| assertEquals(0, mResolver.delete(uri, null, null)); |
| |
| try { |
| assertContentsEquals(new byte[0], uri); |
| fail("Somehow read missing file?"); |
| } catch(FileNotFoundException e) { |
| } |
| } |
| |
| @Test |
| public void testMetaDataTargets() { |
| Uri actual; |
| |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| new File("/proc/version")); |
| assertEquals("content://moocow/test_root/proc/version", actual.toString()); |
| |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| new File("/proc/1/mountinfo")); |
| assertEquals("content://moocow/test_init/mountinfo", actual.toString()); |
| |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(mContext.getFilesDir(), "meow")); |
| assertEquals("content://moocow/test_files/meow", actual.toString()); |
| |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(mContext.getFilesDir(), "thumbs", "rawr")); |
| assertEquals("content://moocow/test_thumbs/rawr", actual.toString()); |
| |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(mContext.getCacheDir(), "up", "down")); |
| assertEquals("content://moocow/test_cache/up/down", actual.toString()); |
| |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(Environment.getExternalStorageDirectory(), "Android", "obb", "foobar")); |
| assertEquals("content://moocow/test_external/Android/obb/foobar", actual.toString()); |
| |
| File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(mContext, null); |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(externalFilesDirs[0], "foo", "bar")); |
| assertEquals("content://moocow/test_external_files/foo/bar", actual.toString()); |
| |
| File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(mContext); |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(externalCacheDirs[0], "foo", "bar")); |
| assertEquals("content://moocow/test_external_cache/foo/bar", actual.toString()); |
| |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| File[] externalMediaDirs = mContext.getExternalMediaDirs(); |
| actual = FileProvider.getUriForFile(mContext, TEST_AUTHORITY, |
| buildPath(externalMediaDirs[0], "foo", "bar")); |
| assertEquals("content://moocow/test_external_media/foo/bar", actual.toString()); |
| } |
| } |
| |
| private void assertContentsEquals(byte[] expected, Uri actual) throws Exception { |
| final InputStream in = mResolver.openInputStream(actual); |
| try { |
| assertArrayEquals(expected, readFully(in)); |
| } finally { |
| closeQuietly(in); |
| } |
| } |
| |
| private Uri stageFileAndGetUri(File file, byte[] data) throws Exception { |
| if (data != null) { |
| final FileOutputStream out = new FileOutputStream(file); |
| try { |
| out.write(data); |
| } finally { |
| out.close(); |
| } |
| } else { |
| file.delete(); |
| } |
| return FileProvider.getUriForFile(mContext, TEST_AUTHORITY, file); |
| } |
| |
| private static File buildPath(File base, String... segments) { |
| File cur = base; |
| for (String segment : segments) { |
| if (cur == null) { |
| cur = new File(segment); |
| } else { |
| cur = new File(cur, segment); |
| } |
| } |
| return cur; |
| } |
| |
| /** |
| * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. |
| */ |
| private static void closeQuietly(AutoCloseable closeable) { |
| if (closeable != null) { |
| try { |
| closeable.close(); |
| } catch (RuntimeException rethrown) { |
| throw rethrown; |
| } catch (Exception ignored) { |
| } |
| } |
| } |
| |
| /** |
| * Returns a byte[] containing the remainder of 'in', closing it when done. |
| */ |
| private static byte[] readFully(InputStream in) throws IOException { |
| try { |
| return readFullyNoClose(in); |
| } finally { |
| in.close(); |
| } |
| } |
| |
| /** |
| * Returns a byte[] containing the remainder of 'in'. |
| */ |
| private static byte[] readFullyNoClose(InputStream in) throws IOException { |
| ByteArrayOutputStream bytes = new ByteArrayOutputStream(); |
| byte[] buffer = new byte[1024]; |
| int count; |
| while ((count = in.read(buffer)) != -1) { |
| bytes.write(buffer, 0, count); |
| } |
| return bytes.toByteArray(); |
| } |
| } |