blob: 177fddb977358e983ef2e91ae3fbe4e6e6591414 [file] [log] [blame]
/*
* Copyright (C) 2009 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 android.provider.cts;
import static android.provider.cts.MediaStoreTest.TAG;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.storage.StorageManager;
import android.platform.test.annotations.Presubmit;
import android.provider.MediaStore;
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.Images.Media;
import android.provider.cts.MediaStoreUtils.PendingParams;
import android.provider.cts.MediaStoreUtils.PendingSession;
import android.util.Log;
import android.util.Size;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.FileUtils;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@Presubmit
@RunWith(Parameterized.class)
public class MediaStore_Images_MediaTest {
private static final String MIME_TYPE_JPEG = "image/jpeg";
private Context mContext;
private ContentResolver mContentResolver;
private Uri mExternalImages;
@Parameter(0)
public String mVolumeName;
@Parameters
public static Iterable<? extends Object> data() {
return ProviderTestUtils.getSharedVolumeNames();
}
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mContentResolver = mContext.getContentResolver();
Log.d(TAG, "Using volume " + mVolumeName);
mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName);
}
@Test
public void testInsertImageWithImagePath() throws Exception {
// TODO: expand test to verify paths from secondary storage devices
if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return;
final long unique1 = System.nanoTime();
final String TEST_TITLE1 = "Title " + unique1;
final long unique2 = System.nanoTime();
final String TEST_TITLE2 = "Title " + unique2;
Cursor c = Media.query(mContentResolver, mExternalImages, null, null,
"_id ASC");
int previousCount = c.getCount();
c.close();
// insert an image by path
File file = new File(ProviderTestUtils.stageDir(MediaStore.VOLUME_EXTERNAL),
"mediaStoreTest1.jpg");
String path = file.getAbsolutePath();
ProviderTestUtils.stageFile(R.raw.scenery, file);
String stringUrl = null;
try {
stringUrl = Media.insertImage(mContentResolver, path, TEST_TITLE1, null);
} catch (FileNotFoundException e) {
fail(e.getMessage());
} catch (UnsupportedOperationException e) {
// the tests will be aborted because the image will be put in sdcard
fail("There is no sdcard attached! " + e.getMessage());
}
assertInsertionSuccess(stringUrl);
// insert another image by path
file = new File(ProviderTestUtils.stageDir(MediaStore.VOLUME_EXTERNAL),
"mediaStoreTest2.jpg");
path = file.getAbsolutePath();
ProviderTestUtils.stageFile(R.raw.scenery, file);
stringUrl = null;
try {
stringUrl = Media.insertImage(mContentResolver, path, TEST_TITLE2, null);
} catch (FileNotFoundException e) {
fail(e.getMessage());
} catch (UnsupportedOperationException e) {
// the tests will be aborted because the image will be put in sdcard
fail("There is no sdcard attached! " + e.getMessage());
}
assertInsertionSuccess(stringUrl);
// query the newly added image
c = Media.query(mContentResolver, Uri.parse(stringUrl),
new String[] { Media.TITLE, Media.DESCRIPTION, Media.MIME_TYPE });
assertEquals(1, c.getCount());
c.moveToFirst();
assertEquals(TEST_TITLE2, c.getString(c.getColumnIndex(Media.TITLE)));
assertEquals(MIME_TYPE_JPEG, c.getString(c.getColumnIndex(Media.MIME_TYPE)));
c.close();
// query all the images in external db and order them by descending id
// (make the images added in test case in the first positions)
c = Media.query(mContentResolver, mExternalImages,
new String[] { Media.TITLE, Media.DESCRIPTION, Media.MIME_TYPE }, null,
"_id DESC");
assertEquals(previousCount + 2, c.getCount());
c.moveToFirst();
assertEquals(TEST_TITLE2, c.getString(c.getColumnIndex(Media.TITLE)));
assertEquals(MIME_TYPE_JPEG, c.getString(c.getColumnIndex(Media.MIME_TYPE)));
c.moveToNext();
assertEquals(TEST_TITLE1, c.getString(c.getColumnIndex(Media.TITLE)));
assertEquals(MIME_TYPE_JPEG, c.getString(c.getColumnIndex(Media.MIME_TYPE)));
c.close();
// query the second image added in the test
c = Media.query(mContentResolver, Uri.parse(stringUrl),
new String[] { Media.DESCRIPTION, Media.MIME_TYPE }, Media.TITLE + "=?",
new String[] { TEST_TITLE2 }, "_id ASC");
assertEquals(1, c.getCount());
c.moveToFirst();
assertEquals(MIME_TYPE_JPEG, c.getString(c.getColumnIndex(Media.MIME_TYPE)));
c.close();
}
@Test
public void testInsertImageWithBitmap() throws Exception {
final long unique3 = System.nanoTime();
final String TEST_TITLE3 = "Title " + unique3;
final String TEST_DESCRIPTION3 = "Description " + unique3;
// insert the image by bitmap
Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery);
String stringUrl = null;
try{
stringUrl = Media.insertImage(mContentResolver, src, TEST_TITLE3, TEST_DESCRIPTION3);
} catch (UnsupportedOperationException e) {
// the tests will be aborted because the image will be put in sdcard
fail("There is no sdcard attached! " + e.getMessage());
}
assertInsertionSuccess(stringUrl);
Cursor c = Media.query(mContentResolver, Uri.parse(stringUrl), new String[] { Media.DATA },
null, "_id ASC");
c.moveToFirst();
// get the bimap by the path
Bitmap result = Media.getBitmap(mContentResolver,
Uri.fromFile(new File(c.getString(c.getColumnIndex(Media.DATA)))));
// can not check the identity between the result and source bitmap because
// source bitmap is compressed before it is saved as result bitmap
assertEquals(src.getWidth(), result.getWidth());
assertEquals(src.getHeight(), result.getHeight());
}
@Presubmit
@Test
public void testGetContentUri() {
Cursor c = null;
assertNotNull(c = mContentResolver.query(Media.getContentUri("internal"), null, null, null,
null));
c.close();
assertNotNull(c = mContentResolver.query(Media.getContentUri(mVolumeName), null, null, null,
null));
c.close();
}
private void cleanExternalMediaFile(String path) {
mContentResolver.delete(mExternalImages, "_data=?", new String[] { path });
new File(path).delete();
}
@Test
public void testStoreImagesMediaExternal() throws Exception {
final String externalPath = new File(ProviderTestUtils.stageDir(mVolumeName),
"testimage.jpg").getAbsolutePath();
final String externalPath2 = new File(ProviderTestUtils.stageDir(mVolumeName),
"testimage1.jpg").getAbsolutePath();
// clean up any potential left over entries from a previous aborted run
cleanExternalMediaFile(externalPath);
cleanExternalMediaFile(externalPath2);
int numBytes = 1337;
FileUtils.createFile(new File(externalPath), numBytes);
ContentValues values = new ContentValues();
values.put(Media.ORIENTATION, 0);
values.put(Media.PICASA_ID, 0);
long dateTaken = System.currentTimeMillis();
values.put(Media.DATE_TAKEN, dateTaken);
values.put(Media.DESCRIPTION, "This is a image");
values.put(Media.IS_PRIVATE, 1);
values.put(Media.MINI_THUMB_MAGIC, 0);
values.put(Media.DATA, externalPath);
values.put(Media.DISPLAY_NAME, "testimage");
values.put(Media.MIME_TYPE, "image/jpeg");
values.put(Media.SIZE, numBytes);
values.put(Media.TITLE, "testimage");
long dateAdded = System.currentTimeMillis() / 1000;
values.put(Media.DATE_ADDED, dateAdded);
long dateModified = System.currentTimeMillis() / 1000;
values.put(Media.DATE_MODIFIED, dateModified);
// insert
Uri uri = mContentResolver.insert(mExternalImages, values);
assertNotNull(uri);
try {
// query
Cursor c = mContentResolver.query(uri, null, null, null, null);
assertEquals(1, c.getCount());
c.moveToFirst();
long id = c.getLong(c.getColumnIndex(Media._ID));
assertTrue(id > 0);
assertEquals(0, c.getInt(c.getColumnIndex(Media.ORIENTATION)));
assertEquals(0, c.getLong(c.getColumnIndex(Media.PICASA_ID)));
assertEquals(dateTaken, c.getLong(c.getColumnIndex(Media.DATE_TAKEN)));
assertEquals("This is a image",
c.getString(c.getColumnIndex(Media.DESCRIPTION)));
assertEquals(1, c.getInt(c.getColumnIndex(Media.IS_PRIVATE)));
assertEquals(0, c.getLong(c.getColumnIndex(Media.MINI_THUMB_MAGIC)));
assertEquals(externalPath, c.getString(c.getColumnIndex(Media.DATA)));
assertEquals("testimage.jpg", c.getString(c.getColumnIndex(Media.DISPLAY_NAME)));
assertEquals("image/jpeg", c.getString(c.getColumnIndex(Media.MIME_TYPE)));
assertEquals("testimage", c.getString(c.getColumnIndex(Media.TITLE)));
assertEquals(numBytes, c.getInt(c.getColumnIndex(Media.SIZE)));
long realDateAdded = c.getLong(c.getColumnIndex(Media.DATE_ADDED));
assertTrue(realDateAdded >= dateAdded);
// there can be delay as time is read after creation
assertTrue(Math.abs(dateModified - c.getLong(c.getColumnIndex(Media.DATE_MODIFIED)))
< 5);
c.close();
} finally {
// delete
assertEquals(1, mContentResolver.delete(uri, null, null));
new File(externalPath).delete();
}
}
private void assertInsertionSuccess(String stringUrl) throws IOException {
final Uri uri = Uri.parse(stringUrl);
// check whether the thumbnails are generated
try (Cursor c = mContentResolver.query(uri, null, null, null)) {
assertEquals(1, c.getCount());
}
assertNotNull(mContentResolver.loadThumbnail(uri, new Size(512, 384), null));
assertNotNull(mContentResolver.loadThumbnail(uri, new Size(96, 96), null));
}
/**
* This test doesn't hold
* {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}, so Exif
* location information should be redacted.
*/
@Test
public void testLocationRedaction() throws Exception {
// STOPSHIP: remove this once isolated storage is always enabled
Assume.assumeTrue(StorageManager.hasIsolatedStorage());
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
mExternalImages, displayName, "image/jpeg");
final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
final Uri publishUri;
try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
try (InputStream in = mContext.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg);
OutputStream out = session.openOutputStream()) {
android.os.FileUtils.copy(in, out);
}
publishUri = session.publish();
}
final Uri originalUri = MediaStore.setRequireOriginal(publishUri);
// Since we own the image, we should be able to see the Exif data that
// we ourselves contributed
try (InputStream is = mContentResolver.openInputStream(publishUri)) {
final ExifInterface exif = new ExifInterface(is);
final float[] latLong = new float[2];
exif.getLatLong(latLong);
assertEquals(53.83451, latLong[0], 0.001);
assertEquals(10.69585, latLong[1], 0.001);
String xmp = exif.getAttribute(ExifInterface.TAG_XMP);
assertTrue("Failed to read XMP longitude", xmp.contains("53,50.070500N"));
assertTrue("Failed to read XMP latitude", xmp.contains("10,41.751000E"));
assertTrue("Failed to read non-location XMP", xmp.contains("LensDefaults"));
}
// As owner, we should be able to request the original bytes
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(originalUri, "r")) {
}
// Now remove ownership, which means that Exif/XMP location data should be redacted
ProviderTestUtils.executeShellCommand(
"content update --uri " + publishUri + " --bind owner_package_name:n:",
InstrumentationRegistry.getInstrumentation().getUiAutomation());
try (InputStream is = mContentResolver.openInputStream(publishUri)) {
final ExifInterface exif = new ExifInterface(is);
final float[] latLong = new float[2];
exif.getLatLong(latLong);
assertEquals(0, latLong[0], 0.001);
assertEquals(0, latLong[1], 0.001);
String xmp = exif.getAttribute(ExifInterface.TAG_XMP);
assertFalse("Failed to redact XMP longitude", xmp.contains("53,50.070500N"));
assertFalse("Failed to redact XMP latitude", xmp.contains("10,41.751000E"));
assertTrue("Redacted non-location XMP", xmp.contains("LensDefaults"));
}
// We can't request original bytes unless we have permission
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(originalUri, "r")) {
fail("Able to read original content without ACCESS_MEDIA_LOCATION");
} catch (UnsupportedOperationException expected) {
}
}
@Test
public void testLocationDeprecated() throws Exception {
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
mExternalImages, displayName, "image/jpeg");
final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
final Uri publishUri;
try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
try (InputStream in = mContext.getResources().openRawResource(R.raw.volantis);
OutputStream out = session.openOutputStream()) {
android.os.FileUtils.copy(in, out);
}
publishUri = session.publish();
}
// Verify that location wasn't indexed
try (Cursor c = mContentResolver.query(publishUri,
new String[] { ImageColumns.LATITUDE, ImageColumns.LONGITUDE }, null, null)) {
assertTrue(c.moveToFirst());
assertTrue(c.isNull(0));
assertTrue(c.isNull(1));
}
// Verify that location values aren't recorded
final ContentValues values = new ContentValues();
values.put(ImageColumns.LATITUDE, 32f);
values.put(ImageColumns.LONGITUDE, 64f);
mContentResolver.update(publishUri, values, null, null);
try (Cursor c = mContentResolver.query(publishUri,
new String[] { ImageColumns.LATITUDE, ImageColumns.LONGITUDE }, null, null)) {
assertTrue(c.moveToFirst());
assertTrue(c.isNull(0));
assertTrue(c.isNull(1));
}
}
@Test
public void testCanonicalize() throws Exception {
// Remove all audio left over from other tests
ProviderTestUtils.executeShellCommand(
"content delete --uri " + mExternalImages,
InstrumentationRegistry.getInstrumentation().getUiAutomation());
// Publish some content
final File dir = ProviderTestUtils.stageDir(mVolumeName);
final Uri a = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.scenery, new File(dir, "a.jpg")));
final Uri b = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.lg_g4_iso_800_jpg, new File(dir, "b.jpg")));
final Uri c = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.scenery, new File(dir, "c.jpg")));
// Confirm we can canonicalize and recover it
final Uri canonicalized = mContentResolver.canonicalize(b);
assertNotNull(canonicalized);
assertEquals(b, mContentResolver.uncanonicalize(canonicalized));
// Delete all items above
mContentResolver.delete(a, null, null);
mContentResolver.delete(b, null, null);
mContentResolver.delete(c, null, null);
// Confirm canonical item isn't found
assertNull(mContentResolver.uncanonicalize(canonicalized));
// Publish data again and confirm we can recover it
final Uri d = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.lg_g4_iso_800_jpg, new File(dir, "d.jpg")));
assertEquals(d, mContentResolver.uncanonicalize(canonicalized));
}
@Test
public void testMetadata() throws Exception {
final Uri uri = ProviderTestUtils.stageMedia(R.raw.lg_g4_iso_800_jpg, mExternalImages,
"image/jpeg");
try (Cursor c = mContentResolver.query(uri, null, null, null)) {
assertTrue(c.moveToFirst());
// Confirm that we parsed Exif metadata
assertEquals(0, c.getLong(c.getColumnIndex(ImageColumns.ORIENTATION)));
assertEquals(600, c.getLong(c.getColumnIndex(ImageColumns.WIDTH)));
assertEquals(337, c.getLong(c.getColumnIndex(ImageColumns.HEIGHT)));
// Confirm that we parsed XMP metadata
assertEquals("xmp.did:041dfd42-0b46-4302-918a-836fba5016ed",
c.getString(c.getColumnIndex(ImageColumns.DOCUMENT_ID)));
assertEquals("xmp.iid:041dfd42-0b46-4302-918a-836fba5016ed",
c.getString(c.getColumnIndex(ImageColumns.INSTANCE_ID)));
assertEquals("3F9DD7A46B26513A7C35272F0D623A06",
c.getString(c.getColumnIndex(ImageColumns.ORIGINAL_DOCUMENT_ID)));
// Confirm that timestamp was parsed with offset information
assertEquals(1447346778000L + 25200000L,
c.getLong(c.getColumnIndex(ImageColumns.DATE_TAKEN)));
// We just added and modified the file, so should be recent
final long added = c.getLong(c.getColumnIndex(ImageColumns.DATE_ADDED));
final long modified = c.getLong(c.getColumnIndex(ImageColumns.DATE_MODIFIED));
final long now = System.currentTimeMillis() / 1000;
assertTrue("Invalid added time " + added, Math.abs(added - now) < 5);
assertTrue("Invalid modified time " + modified, Math.abs(modified - now) < 5);
// Confirm that we trusted value from XMP metadata
assertEquals("image/dng", c.getString(c.getColumnIndex(ImageColumns.MIME_TYPE)));
assertEquals(107704, c.getLong(c.getColumnIndex(ImageColumns.SIZE)));
final String displayName = c.getString(c.getColumnIndex(ImageColumns.DISPLAY_NAME));
assertTrue("Invalid display name " + displayName, displayName.startsWith("cts"));
assertTrue("Invalid display name " + displayName, displayName.endsWith(".jpg"));
}
}
}