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