• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 
17 package com.android.providers.media.scan;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertTrue;
21 import static org.junit.Assert.fail;
22 import static org.junit.Assume.assumeFalse;
23 import static org.junit.Assume.assumeTrue;
24 
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.content.res.AssetFileDescriptor;
30 import android.database.Cursor;
31 import android.drm.DrmConvertedStatus;
32 import android.drm.DrmManagerClient;
33 import android.drm.DrmSupportInfo;
34 import android.net.Uri;
35 import android.os.ParcelFileDescriptor;
36 import android.provider.MediaStore;
37 import android.provider.MediaStore.Files.FileColumns;
38 import android.provider.MediaStore.MediaColumns;
39 import android.system.ErrnoException;
40 import android.system.Os;
41 import android.util.Log;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 import androidx.test.InstrumentationRegistry;
46 import androidx.test.runner.AndroidJUnit4;
47 
48 import com.android.providers.media.R;
49 import com.android.providers.media.util.DatabaseUtils;
50 import com.android.providers.media.util.FileUtils;
51 
52 import org.junit.After;
53 import org.junit.Before;
54 import org.junit.Test;
55 import org.junit.runner.RunWith;
56 
57 import java.io.ByteArrayInputStream;
58 import java.io.File;
59 import java.io.FileDescriptor;
60 import java.io.FileInputStream;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.io.RandomAccessFile;
64 import java.io.SequenceInputStream;
65 import java.nio.charset.StandardCharsets;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Enumeration;
69 import java.util.Iterator;
70 import java.util.List;
71 import java.util.Objects;
72 import java.util.function.Consumer;
73 
74 /**
75  * Verify that we scan various DRM files correctly. This is accomplished by
76  * generating DRM files locally and confirming the scan results.
77  */
78 @RunWith(AndroidJUnit4.class)
79 public class DrmTest {
80     private static final String TAG = "DrmTest";
81 
82     private Context mContext;
83     private ContentResolver mResolver;
84     private DrmManagerClient mClient;
85 
86     private static final String MIME_FORWARD_LOCKED = "application/vnd.oma.drm.message";
87     private static final String MIME_UNSUPPORTED = "unsupported/drm.mimetype";
88 
89     @Before
setUp()90     public void setUp() throws Exception {
91         mContext = InstrumentationRegistry.getContext();
92         mResolver = mContext.getContentResolver();
93         mClient = new DrmManagerClient(mContext);
94     }
95 
96     @After
tearDown()97     public void tearDown() throws Exception {
98         FileUtils.closeQuietly(mClient);
99     }
100 
101     @Test
testForwardLock_Audio()102     public void testForwardLock_Audio() throws Exception {
103         assumeTrue(isForwardLockSupported());
104         doForwardLock("audio/mpeg", R.raw.test_audio, (values) -> {
105             assertEquals(1_045L, (long) values.getAsLong(FileColumns.DURATION));
106             assertEquals(FileColumns.MEDIA_TYPE_AUDIO,
107                     (int) values.getAsInteger(FileColumns.MEDIA_TYPE));
108         });
109     }
110 
111     @Test
testForwardLock_Video()112     public void testForwardLock_Video() throws Exception {
113         assumeTrue(isForwardLockSupported());
114         doForwardLock("video/mp4", R.raw.test_video, (values) -> {
115             assertEquals(40_000L, (long) values.getAsLong(FileColumns.DURATION));
116             assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
117                     (int) values.getAsInteger(FileColumns.MEDIA_TYPE));
118         });
119     }
120 
121     @Test
testForwardLock_Image()122     public void testForwardLock_Image() throws Exception {
123         assumeTrue(isForwardLockSupported());
124         doForwardLock("image/jpeg", R.raw.test_image, (values) -> {
125             // ExifInterface currently doesn't know how to scan DRM images, so
126             // the best we can do is verify the base test metadata
127             assertEquals(FileColumns.MEDIA_TYPE_IMAGE,
128                     (int) values.getAsInteger(FileColumns.MEDIA_TYPE));
129         });
130     }
131 
132     @Test
testForwardLock_Binary()133     public void testForwardLock_Binary() throws Exception {
134         assumeTrue(isForwardLockSupported());
135         doForwardLock("application/octet-stream", R.raw.test_image, null);
136     }
137 
138     /**
139      * Verify that empty files that created with {@link #MIME_FORWARD_LOCKED}
140      * can be adjusted by rescanning to reflect their final MIME type.
141      */
142     @Test
testForwardLock_130680734()143     public void testForwardLock_130680734() throws Exception {
144         assumeTrue(isForwardLockSupported());
145 
146         final ContentValues values = new ContentValues();
147         values.put(MediaColumns.DISPLAY_NAME, "temp" + System.nanoTime() + ".fl");
148         values.put(MediaColumns.MIME_TYPE, MIME_FORWARD_LOCKED);
149         values.put(MediaColumns.IS_PENDING, 1);
150 
151         // Stream our forward-locked file into place
152         final Uri uri = mResolver.insert(
153                 MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
154         try (InputStream dmStream = createDmStream("video/mp4", R.raw.test_video);
155                 ParcelFileDescriptor pfd = mResolver.openFile(uri, "rw", null)) {
156             convertDmToFl(mContext, dmStream, pfd.getFileDescriptor());
157         }
158 
159         // Publish, which will kick off a media scan
160         values.clear();
161         values.put(MediaColumns.IS_PENDING, 0);
162         assertEquals(1, mResolver.update(uri, values, null));
163 
164         // Verify that published item reflects final collection
165         final Uri filesUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri),
166                 ContentUris.parseId(uri));
167         try (Cursor c = mResolver.query(filesUri, null, null, null)) {
168             assertTrue(c.moveToFirst());
169 
170             final String mimeType = c.getString(c.getColumnIndex(FileColumns.MIME_TYPE));
171             // To be consistent with the logic in doForwardLock() below: if the devices does not
172             // handle .fl files we won't consider the test failing, but we also can't carry on here,
173             // thus let's do an AssumptionViolatedException.
174             assumeFalse(MIME_UNSUPPORTED.equals(mimeType));
175             assertEquals("video/mp4", mimeType);
176 
177             assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
178                     c.getInt(c.getColumnIndex(FileColumns.MEDIA_TYPE)));
179         }
180     }
181 
createDmStream(@onNull String mimeType, int resId)182     public @NonNull InputStream createDmStream(@NonNull String mimeType, int resId)
183             throws IOException {
184         List<InputStream> sequence = new ArrayList<>();
185 
186         String dmHeader = "--mime_content_boundary\r\n" +
187                 "Content-Type: " + mimeType + "\r\n" +
188                 "Content-Transfer-Encoding: binary\r\n\r\n";
189         sequence.add(new ByteArrayInputStream(dmHeader.getBytes(StandardCharsets.UTF_8)));
190 
191         AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(resId);
192         FileInputStream body = afd.createInputStream();
193         sequence.add(body);
194 
195         String dmFooter = "\r\n--mime_content_boundary--";
196         sequence.add(new ByteArrayInputStream(dmFooter.getBytes(StandardCharsets.UTF_8)));
197 
198         return new SequenceInputStream(new EnumerationAdapter<InputStream>(sequence.iterator()));
199     }
200 
doForwardLock(String mimeType, int resId, @Nullable Consumer<ContentValues> verifier)201     private void doForwardLock(String mimeType, int resId,
202             @Nullable Consumer<ContentValues> verifier) throws Exception {
203         InputStream dmStream = createDmStream(mimeType, resId);
204 
205         File flPath = new File(mContext.getExternalMediaDirs()[0],
206                 "temp" + System.nanoTime() + ".fl");
207         RandomAccessFile flFile = new RandomAccessFile(flPath, "rw");
208         assertTrue("couldn't convert to fl file",
209                 convertDmToFl(mContext, dmStream, flFile.getFD()));
210         dmStream.close(); // this closes the underlying streams and AFD as well
211         flFile.close();
212 
213         // Scan the DRM file and confirm that it looks sane
214         final Uri flUri = MediaStore.scanFile(mContext.getContentResolver(), flPath);
215         final Uri fileUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(flUri),
216                 ContentUris.parseId(flUri));
217         try (Cursor c = mContext.getContentResolver().query(fileUri, null, null, null)) {
218             assertTrue(c.moveToFirst());
219 
220             final ContentValues values = new ContentValues();
221             DatabaseUtils.copyFromCursorToContentValues(FileColumns.DISPLAY_NAME, c, values);
222             DatabaseUtils.copyFromCursorToContentValues(FileColumns.MIME_TYPE, c, values);
223             DatabaseUtils.copyFromCursorToContentValues(FileColumns.MEDIA_TYPE, c, values);
224             DatabaseUtils.copyFromCursorToContentValues(FileColumns.IS_DRM, c, values);
225             DatabaseUtils.copyFromCursorToContentValues(FileColumns.DURATION, c, values);
226             Log.v(TAG, values.toString());
227 
228             // Filename should match what we found on disk
229             assertEquals(flPath.getName(), values.get(FileColumns.DISPLAY_NAME));
230             // Should always be marked as a DRM file
231             assertEquals("1", values.get(FileColumns.IS_DRM));
232 
233             final String actualMimeType = values.getAsString(FileColumns.MIME_TYPE);
234             if (Objects.equals(mimeType, actualMimeType)) {
235                 // We scanned the item successfully, so we can also check our
236                 // custom verifier, if any
237                 if (verifier != null) {
238                     verifier.accept(values);
239                 }
240             } else if (Objects.equals(MIME_UNSUPPORTED, actualMimeType)) {
241                 // We don't scan unsupported items, so we can't check our custom
242                 // verifier, but we're still willing to consider this as passing.
243             } else {
244                 fail("Unexpected MIME type " + actualMimeType);
245             }
246         }
247     }
248 
249     /**
250      * Shamelessly copied from
251      * cts/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java
252      */
convertDmToFl( Context context, InputStream dmStream, FileDescriptor fd)253     public static boolean convertDmToFl(
254             Context context,
255             InputStream dmStream,
256             FileDescriptor fd) {
257         final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message";
258         byte[] dmData = new byte[10000];
259         int totalRead = 0;
260         int numRead;
261         while (true) {
262             try {
263                 numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead);
264             } catch (IOException e) {
265                 Log.w(TAG, "Failed to read from input file");
266                 return false;
267             }
268             if (numRead == -1) {
269                 break;
270             }
271             totalRead += numRead;
272             if (totalRead == dmData.length) {
273                 // grow array
274                 dmData = Arrays.copyOf(dmData, dmData.length + 10000);
275             }
276         }
277         byte[] fileData = Arrays.copyOf(dmData, totalRead);
278 
279         DrmManagerClient drmClient = null;
280         try {
281             drmClient = new DrmManagerClient(context);
282         } catch (IllegalArgumentException e) {
283             Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal.");
284             return false;
285         } catch (IllegalStateException e) {
286             Log.w(TAG, "DrmManagerClient didn't initialize properly.");
287             return false;
288         }
289 
290         try {
291             int convertSessionId = -1;
292             try {
293                 convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE);
294             } catch (IllegalArgumentException e) {
295                 Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE
296                         + " is not supported.", e);
297                 return false;
298             } catch (IllegalStateException e) {
299                 Log.w(TAG, "Could not access Open DrmFramework.", e);
300                 return false;
301             }
302 
303             if (convertSessionId < 0) {
304                 Log.w(TAG, "Failed to open session.");
305                 return false;
306             }
307 
308             DrmConvertedStatus convertedStatus = null;
309             try {
310                 convertedStatus = drmClient.convertData(convertSessionId, fileData);
311             } catch (IllegalArgumentException e) {
312                 Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: "
313                         + convertSessionId, e);
314                 return false;
315             } catch (IllegalStateException e) {
316                 Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e);
317                 return false;
318             }
319 
320             if (convertedStatus == null ||
321                     convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
322                     convertedStatus.convertedData == null) {
323                 Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId);
324                 try {
325                     DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId);
326                     if (result.statusCode != DrmConvertedStatus.STATUS_OK) {
327                         Log.w(TAG, "Conversion failed with status: " + result.statusCode);
328                         return false;
329                     }
330                 } catch (IllegalStateException e) {
331                     Log.w(TAG, "Could not close session. Convertsession: " +
332                            convertSessionId, e);
333                 }
334                 return false;
335             }
336 
337             try {
338                 Os.write(fd, convertedStatus.convertedData, 0,
339                         convertedStatus.convertedData.length);
340             } catch (IOException | ErrnoException e) {
341                 Log.w(TAG, "Failed to write to output file: " + e);
342                 return false;
343             }
344 
345             try {
346                 convertedStatus = drmClient.closeConvertSession(convertSessionId);
347             } catch (IllegalStateException e) {
348                 Log.w(TAG, "Could not close convertsession. Convertsession: " +
349                         convertSessionId, e);
350                 return false;
351             }
352 
353             if (convertedStatus == null ||
354                     convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
355                     convertedStatus.convertedData == null) {
356                 Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId);
357                 return false;
358             }
359 
360             try {
361                 Os.pwrite(fd, convertedStatus.convertedData, 0,
362                         convertedStatus.convertedData.length, convertedStatus.offset);
363             } catch (IOException | ErrnoException e) {
364                 Log.w(TAG, "Could not update file.", e);
365                 return false;
366             }
367 
368             return true;
369         } finally {
370             drmClient.close();
371         }
372     }
373 
isForwardLockSupported()374     private boolean isForwardLockSupported() {
375         for (DrmSupportInfo info : mClient.getAvailableDrmSupportInfo()) {
376             Iterator<String> it = info.getMimeTypeIterator();
377             while (it.hasNext()) {
378                 if (Objects.equals(MIME_FORWARD_LOCKED, it.next())) {
379                     return true;
380                 }
381             }
382         }
383         return false;
384     }
385 
386     /**
387      * This is purely an adapter to convert modern {@link Iterator} back into an
388      * {@link Enumeration} for legacy code.
389      */
390     @SuppressWarnings("JdkObsolete")
391     private static class EnumerationAdapter<T> implements Enumeration<T> {
392         private final Iterator<T> it;
393 
EnumerationAdapter(Iterator<T> it)394         public EnumerationAdapter(Iterator<T> it) {
395             this.it = it;
396         }
397 
398         @Override
hasMoreElements()399         public boolean hasMoreElements() {
400             return it.hasNext();
401         }
402 
403         @Override
nextElement()404         public T nextElement() {
405             return it.next();
406         }
407     }
408 }
409