blob: 0fa99ca07498c16e15aa5070d0612652d20b0340 [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.media;
import static android.provider.cts.ProviderTestUtils.assertExists;
import static android.provider.cts.ProviderTestUtils.assertNotExists;
import static android.provider.cts.media.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.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Environment;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.storage.StorageManager;
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.Video.Media;
import android.provider.MediaStore.Video.VideoColumns;
import android.provider.cts.ProviderTestUtils;
import android.provider.cts.R;
import android.provider.cts.media.MediaStoreUtils.PendingParams;
import android.provider.cts.media.MediaStoreUtils.PendingSession;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
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.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
@RunWith(Parameterized.class)
public class MediaStore_Video_MediaTest {
private Context mContext;
private ContentResolver mContentResolver;
private Uri mExternalVideo;
private Uri mExternalFiles;
@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);
mExternalVideo = MediaStore.Video.Media.getContentUri(mVolumeName);
mExternalFiles = MediaStore.Files.getContentUri(mVolumeName);
}
@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();
assertEquals(ContentUris.withAppendedId(Media.getContentUri(mVolumeName), 42),
Media.getContentUri(mVolumeName, 42));
}
private void cleanExternalMediaFile(String path) {
mContentResolver.delete(mExternalVideo, "_data=?", new String[] { path });
new File(path).delete();
}
@Test
public void testStoreVideoMediaExternal() throws Exception {
final File dir = ProviderTestUtils.stageDir(mVolumeName);
final File videoFile = ProviderTestUtils.stageFile(R.raw.testvideo,
new File(dir, "cts" + System.nanoTime() + ".mp4"));
final String externalVideoPath = videoFile.getAbsolutePath();
final long numBytes = videoFile.length();
ProviderTestUtils.waitUntilExists(videoFile);
ContentValues values = new ContentValues();
values.put(Media.ALBUM, "cts");
values.put(Media.ARTIST, "cts team");
values.put(Media.CATEGORY, "test");
long dateTaken = System.currentTimeMillis();
values.put(Media.DATE_TAKEN, dateTaken);
values.put(Media.DESCRIPTION, "This is a video");
values.put(Media.DURATION, 8480);
values.put(Media.LANGUAGE, "en");
values.put(Media.IS_PRIVATE, 1);
values.put(Media.MINI_THUMB_MAGIC, 0);
values.put(Media.RESOLUTION, "176x144");
values.put(Media.TAGS, "cts, test");
values.put(Media.DATA, externalVideoPath);
values.put(Media.DISPLAY_NAME, "testvideo.3gp");
values.put(Media.MIME_TYPE, "video/3gpp");
values.put(Media.SIZE, numBytes);
values.put(Media.TITLE, "testvideo");
// insert
Uri uri = mContentResolver.insert(mExternalVideo, 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("cts", c.getString(c.getColumnIndex(Media.ALBUM)));
assertEquals("cts team", c.getString(c.getColumnIndex(Media.ARTIST)));
assertEquals("test", c.getString(c.getColumnIndex(Media.CATEGORY)));
assertEquals(dateTaken, c.getLong(c.getColumnIndex(Media.DATE_TAKEN)));
assertEquals(8480, c.getInt(c.getColumnIndex(Media.DURATION)));
assertEquals("This is a video",
c.getString(c.getColumnIndex(Media.DESCRIPTION)));
assertEquals("en", c.getString(c.getColumnIndex(Media.LANGUAGE)));
assertEquals(1, c.getInt(c.getColumnIndex(Media.IS_PRIVATE)));
assertEquals(0, c.getLong(c.getColumnIndex(Media.MINI_THUMB_MAGIC)));
assertEquals("176x144", c.getString(c.getColumnIndex(Media.RESOLUTION)));
assertEquals("cts, test", c.getString(c.getColumnIndex(Media.TAGS)));
assertEquals(externalVideoPath, c.getString(c.getColumnIndex(Media.DATA)));
assertEquals(videoFile.getName(), c.getString(c.getColumnIndex(Media.DISPLAY_NAME)));
assertEquals("video/3gpp", c.getString(c.getColumnIndex(Media.MIME_TYPE)));
assertEquals("testvideo", c.getString(c.getColumnIndex(Media.TITLE)));
assertEquals(numBytes, c.getInt(c.getColumnIndex(Media.SIZE)));
assertTrue(c.isNull(c.getColumnIndex(Media.COLOR_STANDARD)));
assertTrue(c.isNull(c.getColumnIndex(Media.COLOR_TRANSFER)));
assertTrue(c.isNull(c.getColumnIndex(Media.COLOR_RANGE)));
c.close();
} finally {
// delete
assertEquals(1, mContentResolver.delete(uri, null, null));
new File(externalVideoPath).delete();
}
// check that the video file is removed when deleting the database entry
Context context = mContext;
Uri videoUri = insertVideo(context);
File videofile = new File(ProviderTestUtils.stageDir(mVolumeName), "testVideo.3gp");
assertExists(videofile);
mContentResolver.delete(videoUri, null, null);
assertNotExists(videofile);
}
private Uri insertVideo(Context context) throws IOException {
final File dir = ProviderTestUtils.stageDir(mVolumeName);
final File file = new File(dir, "testVideo.3gp");
// clean up any potential left over entries from a previous aborted run
cleanExternalMediaFile(file.getAbsolutePath());
ProviderTestUtils.stageFile(R.raw.testvideo, file);
ContentValues values = new ContentValues();
values.put(VideoColumns.DATA, file.getAbsolutePath());
return context.getContentResolver().insert(mExternalVideo, values);
}
@Test
public void testOriginalAccess() throws Exception {
final Uri publishUri = ProviderTestUtils.stageMedia(R.raw.testvideo_meta, mExternalVideo,
"video/mp4");
final Uri originalUri = MediaStore.setRequireOriginal(publishUri);
// As owner, we should be able to request the original bytes
try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(originalUri, "r")) {
}
// Revoke location access and remove ownership, which means that location should be redacted
ProviderTestUtils.revokeMediaLocationPermission(mContext);
ProviderTestUtils.clearOwner(publishUri);
// 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 testXmpLocationRedaction() throws Exception {
final Uri publishUri = ProviderTestUtils.stageMedia(R.raw.testvideo_meta, mExternalVideo,
"video/mp4");
try (InputStream in = mContentResolver.openInputStream(publishUri);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
FileUtils.copy(in, out);
byte[] bytes = out.toByteArray();
byte[] xmpBytes = Arrays.copyOfRange(bytes, 3269, 3269 + 13197);
String xmp = new String(xmpBytes);
assertTrue("Failed to read XMP longitude", xmp.contains("10,41.751000E"));
assertTrue("Failed to read XMP latitude", xmp.contains("53,50.070500N"));
assertTrue("Failed to read non-location XMP", xmp.contains("13166/7763"));
}
// Revoke location access and remove ownership, which means that location should be redacted
ProviderTestUtils.revokeMediaLocationPermission(mContext);
ProviderTestUtils.clearOwner(publishUri);
try (InputStream in = mContentResolver.openInputStream(publishUri);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
FileUtils.copy(in, out);
byte[] bytes = out.toByteArray();
byte[] xmpBytes = Arrays.copyOfRange(bytes, 3269, 3269 + 13197);
String xmp = new String(xmpBytes);
assertFalse("Failed to redact XMP longitude", xmp.contains("10,41.751000E"));
assertFalse("Failed to redact XMP latitude", xmp.contains("53,50.070500N"));
assertTrue("Redacted non-location XMP", xmp.contains("13166/7763"));
}
}
@Test
public void testIsoLocationRedaction() throws Exception {
// STOPSHIP: remove this once isolated storage is always enabled
Assume.assumeTrue(StorageManager.hasIsolatedStorage());
final Uri publishUri = ProviderTestUtils.stageMedia(R.raw.testvideo_meta, mExternalVideo,
"video/mp4");
// Since we own the video, we should be able to see the location
// we ourselves contributed
try (ParcelFileDescriptor pfd = mContentResolver.openFile(publishUri, "r", null);
MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
mmr.setDataSource(pfd.getFileDescriptor());
assertEquals("+37.4217-122.0834/",
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION));
assertEquals("2", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS));
}
// Revoke location access and remove ownership, which means that location should be redacted
ProviderTestUtils.revokeMediaLocationPermission(mContext);
ProviderTestUtils.clearOwner(publishUri);
try (ParcelFileDescriptor pfd = mContentResolver.openFile(publishUri, "r", null);
MediaMetadataRetriever mmr = new MediaMetadataRetriever()) {
mmr.setDataSource(pfd.getFileDescriptor());
assertEquals(null,
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION));
assertEquals("2", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS));
}
}
@Test
public void testLocationDeprecated() throws Exception {
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
mExternalVideo, displayName, "video/mp4");
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.testvideo_meta);
OutputStream out = session.openOutputStream()) {
FileUtils.copy(in, out);
}
publishUri = session.publish();
}
// Verify that location wasn't indexed
try (Cursor c = mContentResolver.query(publishUri,
new String[] { VideoColumns.LATITUDE, VideoColumns.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(VideoColumns.LATITUDE, 32f);
values.put(VideoColumns.LONGITUDE, 64f);
mContentResolver.update(publishUri, values, null, null);
try (Cursor c = mContentResolver.query(publishUri,
new String[] { VideoColumns.LATITUDE, VideoColumns.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"
+ " --user " + InstrumentationRegistry.getTargetContext().getUserId()
+ " --uri " + mExternalVideo,
InstrumentationRegistry.getInstrumentation().getUiAutomation());
// Publish some content
final File dir = ProviderTestUtils.stageDir(mVolumeName);
final Uri a = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.testvideo, new File(dir, "a.mp4")));
final Uri b = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.testvideo_meta, new File(dir, "b.mp4")));
final Uri c = ProviderTestUtils.scanFileFromShell(
ProviderTestUtils.stageFile(R.raw.testvideo, new File(dir, "c.mp4")));
// 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.testvideo_meta, new File(dir, "d.mp4")));
assertEquals(d, mContentResolver.uncanonicalize(canonicalized));
}
@Test
public void testMetadata() throws Exception {
final Uri uri = ProviderTestUtils.stageMedia(R.raw.testvideo_meta, mExternalVideo,
"video/mp4");
try (Cursor c = mContentResolver.query(uri, null, null, null)) {
assertTrue(c.moveToFirst());
// Confirm that we parsed Exif metadata
assertEquals(9296, c.getLong(c.getColumnIndex(VideoColumns.DURATION)));
assertEquals(1920, c.getLong(c.getColumnIndex(VideoColumns.WIDTH)));
assertEquals(1080, c.getLong(c.getColumnIndex(VideoColumns.HEIGHT)));
assertEquals(90, c.getInt(c.getColumnIndex(VideoColumns.ORIENTATION)));
// Confirm that we parsed XMP metadata
assertEquals("xmp.did:051dfd42-0b46-4302-918a-836fba5016ed",
c.getString(c.getColumnIndex(VideoColumns.DOCUMENT_ID)));
assertEquals("xmp.iid:051dfd42-0b46-4302-918a-836fba5016ed",
c.getString(c.getColumnIndex(VideoColumns.INSTANCE_ID)));
assertEquals("4F9DD7A46B26513A7C35272F0D623A06",
c.getString(c.getColumnIndex(VideoColumns.ORIGINAL_DOCUMENT_ID)));
assertTrue(new String(c.getBlob(c.getColumnIndex(VideoColumns.XMP)))
.contains("exif:ShutterSpeedValue"));
// Confirm that we redacted location metadata
assertFalse(new String(c.getBlob(c.getColumnIndex(VideoColumns.XMP)))
.contains("exif:GPSLatitude"));
assertFalse(new String(c.getBlob(c.getColumnIndex(VideoColumns.XMP)))
.contains("exif:GPSLongitude"));
// Confirm that timestamp was parsed
assertEquals(1539711603000L, c.getLong(c.getColumnIndex(VideoColumns.DATE_TAKEN)));
// We just added and modified the file, so should be recent
final long added = c.getLong(c.getColumnIndex(VideoColumns.DATE_ADDED));
final long modified = c.getLong(c.getColumnIndex(VideoColumns.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("video/dng", c.getString(c.getColumnIndex(VideoColumns.MIME_TYPE)));
assertEquals(20716, c.getLong(c.getColumnIndex(VideoColumns.SIZE)));
final String displayName = c.getString(c.getColumnIndex(VideoColumns.DISPLAY_NAME));
assertTrue("Invalid display name " + displayName, displayName.startsWith("cts"));
assertTrue("Invalid display name " + displayName, displayName.endsWith(".mp4"));
}
}
/**
* Confirm that we can place both media and subtitles together in the same
* location on disk.
*/
@Test
public void testMediaWithSubtitles() throws Exception {
final String displayName = "cts" + System.nanoTime();
final String mediaDisplayName = displayName + ".mp4";
final String subDisplayName = displayName + ".srt";
final PendingParams media = new PendingParams(
mExternalVideo, mediaDisplayName, "video/mp4");
final PendingParams sub = new PendingParams(
mExternalFiles, subDisplayName, "application/x-subrip");
media.setPath(Environment.DIRECTORY_MOVIES);
sub.setPath(Environment.DIRECTORY_MOVIES);
final Uri mediaUri = ContentUris.withAppendedId(mExternalFiles,
ContentUris.parseId(execPending(media)));
final Uri subUri = ContentUris.withAppendedId(mExternalFiles,
ContentUris.parseId(execPending(sub)));
final String[] projection = new String[] {
FileColumns.RELATIVE_PATH,
FileColumns.DISPLAY_NAME,
FileColumns.MEDIA_TYPE
};
// Confirm both files landed in same path
try (Cursor c = mContentResolver.query(mediaUri, projection, null, null)) {
assertTrue(c.moveToFirst());
assertEquals(Environment.DIRECTORY_MOVIES + '/', c.getString(0));
assertEquals(mediaDisplayName, c.getString(1));
assertEquals(FileColumns.MEDIA_TYPE_VIDEO, c.getInt(2));
}
try (Cursor c = mContentResolver.query(subUri, projection, null, null)) {
assertTrue(c.moveToFirst());
assertEquals(Environment.DIRECTORY_MOVIES + '/', c.getString(0));
assertEquals(subDisplayName, c.getString(1));
assertEquals(FileColumns.MEDIA_TYPE_SUBTITLE, c.getInt(2));
}
}
private Uri execPending(PendingParams params) throws Exception {
final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
try (OutputStream out = session.openOutputStream()) {
out.write((int) 42);
}
return session.publish();
}
}
}