1 /*
2  * Copyright 2014 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 package com.android.compatibility.common.util;
17 
18 import android.content.Context;
19 import android.content.Intent;
20 import android.content.IntentFilter;
21 import android.content.pm.PackageManager;
22 import android.content.res.AssetFileDescriptor;
23 import android.drm.DrmConvertedStatus;
24 import android.drm.DrmManagerClient;
25 import android.graphics.ImageFormat;
26 import android.graphics.Rect;
27 import android.media.Image;
28 import android.media.Image.Plane;
29 import android.media.MediaCodec;
30 import android.media.MediaCodec.BufferInfo;
31 import android.media.MediaCodecInfo;
32 import android.media.MediaCodecInfo.CodecCapabilities;
33 import android.media.MediaCodecInfo.VideoCapabilities;
34 import android.media.MediaCodecList;
35 import android.media.MediaExtractor;
36 import android.media.MediaFormat;
37 import android.net.Uri;
38 import android.os.BatteryManager;
39 import android.os.Build;
40 import android.os.SystemProperties;
41 import android.os.ParcelFileDescriptor;
42 import android.util.DisplayMetrics;
43 import android.util.Log;
44 import android.util.Range;
45 import android.util.Size;
46 import android.view.WindowManager;
47 
48 import androidx.test.platform.app.InstrumentationRegistry;
49 
50 import com.android.compatibility.common.util.DeviceReportLog;
51 import com.android.compatibility.common.util.ResultType;
52 import com.android.compatibility.common.util.ResultUnit;
53 
54 import java.io.File;
55 import java.lang.reflect.Method;
56 import java.nio.ByteBuffer;
57 import java.security.MessageDigest;
58 
59 import static java.lang.reflect.Modifier.isPublic;
60 import static java.lang.reflect.Modifier.isStatic;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.List;
64 import java.util.Map;
65 
66 import static junit.framework.Assert.assertTrue;
67 
68 import java.io.IOException;
69 import java.io.InputStream;
70 import java.io.RandomAccessFile;
71 
72 public class MediaUtils {
73     private static final String TAG = "MediaUtils";
74     private static final Context mContext =
75             InstrumentationRegistry.getInstrumentation().getTargetContext();
76     private static final PackageManager pm = mContext.getPackageManager();
77     private static final boolean FIRST_SDK_IS_AT_LEAST_R =
78             ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.R);
79 
80     /*
81      *  ----------------------- HELPER METHODS FOR SKIPPING TESTS -----------------------
82      */
83     private static final int ALL_AV_TRACKS = -1;
84 
85     private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
86 
87     /**
88      * Returns the test name (heuristically).
89      *
90      * Since it uses heuristics, this method has only been verified for media
91      * tests. This centralizes the way to signal errors during a test.
92      */
getTestName()93     public static String getTestName() {
94         return getTestName(false /* withClass */);
95     }
96 
97     /**
98      * Returns the test name with the full class (heuristically).
99      *
100      * Since it uses heuristics, this method has only been verified for media
101      * tests. This centralizes the way to signal errors during a test.
102      */
getTestNameWithClass()103     public static String getTestNameWithClass() {
104         return getTestName(true /* withClass */);
105     }
106 
getTestName(boolean withClass)107     private static String getTestName(boolean withClass) {
108         int bestScore = -1;
109         String testName = "test???";
110         Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
111         for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) {
112             StackTraceElement[] stack = entry.getValue();
113             for (int index = 0; index < stack.length; ++index) {
114                 // method name must start with "test"
115                 String methodName = stack[index].getMethodName();
116                 if (!methodName.startsWith("test")) {
117                     continue;
118                 }
119 
120                 int score = 0;
121                 // see if there is a public non-static void method that takes no argument
122                 Class<?> clazz;
123                 try {
124                     clazz = Class.forName(stack[index].getClassName());
125                     ++score;
126                     for (final Method method : clazz.getDeclaredMethods()) {
127                         if (method.getName().equals(methodName)
128                                 && isPublic(method.getModifiers())
129                                 && !isStatic(method.getModifiers())
130                                 && method.getParameterTypes().length == 0
131                                 && method.getReturnType().equals(Void.TYPE)) {
132                             ++score;
133                             break;
134                         }
135                     }
136                     if (score == 1) {
137                         // if we could read the class, but method is not public void, it is
138                         // not a candidate
139                         continue;
140                     }
141                 } catch (ClassNotFoundException e) {
142                 }
143 
144                 // even if we cannot verify the method signature, there are signals in the stack
145 
146                 // usually test method is invoked by reflection
147                 int depth = 1;
148                 while (index + depth < stack.length
149                         && stack[index + depth].getMethodName().equals("invoke")
150                         && stack[index + depth].getClassName().equals(
151                                 "java.lang.reflect.Method")) {
152                     ++depth;
153                 }
154                 if (depth > 1) {
155                     ++score;
156                     // and usually test method is run by runMethod method in android.test package
157                     if (index + depth < stack.length) {
158                         if (stack[index + depth].getClassName().startsWith("android.test.")) {
159                             ++score;
160                         }
161                         if (stack[index + depth].getMethodName().equals("runMethod")) {
162                             ++score;
163                         }
164                     }
165                 }
166 
167                 if (score > bestScore) {
168                     bestScore = score;
169                     testName = methodName;
170                     if (withClass) {
171                         testName = stack[index].getClassName() + "." + testName;
172                     }
173                 }
174             }
175         }
176         return testName;
177     }
178 
179     /**
180      * Finds test name (heuristically) and prints out standard skip message.
181      *
182      * Since it uses heuristics, this method has only been verified for media
183      * tests. This centralizes the way to signal a skipped test.
184      */
skipTest(String tag, String reason)185     public static void skipTest(String tag, String reason) {
186         Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason);
187         DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped");
188         try {
189             log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE);
190             log.addValue(
191                     "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE);
192             log.submit();
193         } catch (NullPointerException e) { }
194     }
195 
196     /**
197      * Finds test name (heuristically) and prints out standard skip message.
198      *
199      * Since it uses heuristics, this method has only been verified for media
200      * tests.  This centralizes the way to signal a skipped test.
201      */
skipTest(String reason)202     public static void skipTest(String reason) {
203         skipTest(TAG, reason);
204     }
205 
check(boolean result, String message)206     public static boolean check(boolean result, String message) {
207         if (!result) {
208             skipTest(message);
209         }
210         return result;
211     }
212 
213     /*
214      *  ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT -------------------
215      */
216 
isGoogle(String codecName)217     public static boolean isGoogle(String codecName) {
218         codecName = codecName.toLowerCase();
219         return codecName.startsWith("omx.google.")
220                 || codecName.startsWith("c2.android.")
221                 || codecName.startsWith("c2.google.");
222     }
223 
224     // returns the list of codecs that support any one of the formats
getCodecNames( boolean isEncoder, Boolean isGoog, MediaFormat... formats)225     private static String[] getCodecNames(
226             boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
227         ArrayList<String> result = new ArrayList<>();
228         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
229             if (info.isAlias()) {
230                 // don't consider aliases here
231                 continue;
232             }
233             if (info.isEncoder() != isEncoder) {
234                 continue;
235             }
236             if (isGoog != null && isGoogle(info.getName()) != isGoog) {
237                 continue;
238             }
239 
240             for (MediaFormat format : formats) {
241                 String mime = format.getString(MediaFormat.KEY_MIME);
242 
243                 CodecCapabilities caps = null;
244                 try {
245                     caps = info.getCapabilitiesForType(mime);
246                 } catch (IllegalArgumentException e) {  // mime is not supported
247                     continue;
248                 }
249                 if (caps.isFormatSupported(format)) {
250                     result.add(info.getName());
251                     break;
252                 }
253             }
254         }
255         return result.toArray(new String[result.size()]);
256     }
257 
258     /* Use isGoog = null to query all decoders */
getDecoderNames( Boolean isGoog, MediaFormat... formats)259     public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
260         return getCodecNames(false /* isEncoder */, isGoog, formats);
261     }
262 
getDecoderNames(MediaFormat... formats)263     public static String[] getDecoderNames(MediaFormat... formats) {
264         return getCodecNames(false /* isEncoder */, null /* isGoog */, formats);
265     }
266 
267     /* Use isGoog = null to query all decoders */
getEncoderNames( Boolean isGoog, MediaFormat... formats)268     public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
269         return getCodecNames(true /* isEncoder */, isGoog, formats);
270     }
271 
getEncoderNames(MediaFormat... formats)272     public static String[] getEncoderNames(MediaFormat... formats) {
273         return getCodecNames(true /* isEncoder */, null /* isGoog */, formats);
274     }
275 
getDecoderNamesForMime(String mime)276     public static String[] getDecoderNamesForMime(String mime) {
277         MediaFormat format = new MediaFormat();
278         format.setString(MediaFormat.KEY_MIME, mime);
279         return getCodecNames(false /* isEncoder */, null /* isGoog */, format);
280     }
281 
getEncoderNamesForMime(String mime)282     public static String[] getEncoderNamesForMime(String mime) {
283         MediaFormat format = new MediaFormat();
284         format.setString(MediaFormat.KEY_MIME, mime);
285         return getCodecNames(true /* isEncoder */, null /* isGoog */, format);
286     }
287 
verifyNumCodecs( int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats)288     public static void verifyNumCodecs(
289             int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
290         String desc = (isEncoder ? "encoders" : "decoders") + " for "
291                 + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats));
292         if (isGoog != null) {
293             desc = (isGoog ? "Google " : "non-Google ") + desc;
294         }
295 
296         String[] codecs = getCodecNames(isEncoder, isGoog, formats);
297         assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": "
298                 + Arrays.toString(codecs), codecs.length <= count);
299     }
300 
getDecoder(MediaFormat format)301     public static MediaCodec getDecoder(MediaFormat format) {
302         String decoder = sMCL.findDecoderForFormat(format);
303         if (decoder != null) {
304             try {
305                 return MediaCodec.createByCodecName(decoder);
306             } catch (IOException e) {
307             }
308         }
309         return null;
310     }
311 
canEncode(MediaFormat format)312     public static boolean canEncode(MediaFormat format) {
313         if (sMCL.findEncoderForFormat(format) == null) {
314             Log.i(TAG, "no encoder for " + format);
315             return false;
316         }
317         return true;
318     }
319 
canDecode(MediaFormat format)320     public static boolean canDecode(MediaFormat format) {
321         return canDecode(format, 0.0);
322     }
323 
324     // this is "do we claim to decode"; caller is on the hook to determine
325     // if we actually meet that claim, specifically around speed.
canDecode(MediaFormat format, double rate )326     public static boolean canDecode(MediaFormat format, double rate ) {
327         String decoder = sMCL.findDecoderForFormat(format);
328 
329         if (decoder == null) {
330             Log.i(TAG, "no decoder for " + format);
331             return false;
332         }
333 
334         if (rate == 0.0) {
335             return true;
336         }
337 
338         // before Q, we always said yes once we found a decoder for the format.
339         if (ApiLevelUtil.isBefore(Build.VERSION_CODES.Q)) {
340             return true;
341         }
342 
343         // we care about speed of decoding
344         Log.d(TAG, "checking for decoding " + format + " at " +
345                    rate + " fps with " + decoder);
346 
347         String mime = format.getString(MediaFormat.KEY_MIME);
348         int width = format.getInteger(MediaFormat.KEY_WIDTH);
349         int height = format.getInteger(MediaFormat.KEY_HEIGHT);
350 
351         MediaCodecInfo[] mciList = sMCL.getCodecInfos();
352 
353         if (mciList == null) {
354             Log.d(TAG, "did not get list of MediaCodecInfo");
355             return false;
356         }
357 
358         MediaCodecInfo mci = null;
359         for (MediaCodecInfo mci2 : mciList) {
360             if (mci2.getName().equals(decoder)) {
361                 mci = mci2;
362                 break;
363             }
364         }
365         if (mci == null) {
366             return false;
367         }
368         if (!mci.getName().equals(decoder)) {
369             Log.e(TAG, "did not find expected " + decoder);
370             return false;
371         }
372 
373         if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q)
374                 && PropertyUtil.isVendorApiLevelAtLeast(Build.VERSION_CODES.Q)
375                 && mci.isHardwareAccelerated()) {
376             MediaCodecInfo.VideoCapabilities caps =
377                             mci.getCapabilitiesForType(mime).getVideoCapabilities();
378             List<MediaCodecInfo.VideoCapabilities.PerformancePoint> pp =
379                             caps.getSupportedPerformancePoints();
380             VideoCapabilities.PerformancePoint target =
381                             new VideoCapabilities.PerformancePoint(width, height, (int) rate);
382             for (MediaCodecInfo.VideoCapabilities.PerformancePoint point : pp) {
383                 if (point.covers(target)) {
384                     Log.i(TAG, "target " + target.toString() +
385                                " covered by point " + point.toString());
386                     return true;
387                 }
388             }
389             Log.i(TAG, "NOT covered by any hardware performance point");
390             return false;
391         } else {
392             String verified = MediaPerfUtils.areAchievableFrameRates(
393                               decoder, mime, width, height, rate);
394             if (verified == null) {
395                 Log.d(TAG, "claims to decode content at " + rate + " fps");
396                 return true;
397             }
398             Log.d(TAG, "achieveable framerates says: " + verified);
399             return false;
400         }
401     }
402 
supports(String codecName, String mime, int w, int h)403     public static boolean supports(String codecName, String mime, int w, int h) {
404         // While this could be simply written as such, give more graceful feedback.
405         // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h);
406         // return supports(codecName, format);
407 
408         VideoCapabilities vidCap = getVideoCapabilities(codecName, mime);
409         if (vidCap == null) {
410             return false;
411         } else if (vidCap.isSizeSupported(w, h)) {
412             return true;
413         }
414 
415         Log.w(TAG, "unsupported size " + w + "x" + h);
416         return false;
417     }
418 
419     /**
420      * returns true if the given codec component of mediaType supports the given resolution
421      */
supports(String codecName, String mediaType, Size size)422     public static boolean supports(String codecName, String mediaType, Size size) {
423         return supports(codecName, mediaType, size.getWidth(), size.getHeight());
424     }
425 
supports(String codecName, MediaFormat format)426     public static boolean supports(String codecName, MediaFormat format) {
427         MediaCodec codec;
428         try {
429             codec = MediaCodec.createByCodecName(codecName);
430         } catch (IOException e) {
431             Log.w(TAG, "codec not found: " + codecName);
432             return false;
433         } catch (NullPointerException e) {
434             Log.w(TAG, "codec name is null");
435             return false;
436         }
437 
438         String mime = format.getString(MediaFormat.KEY_MIME);
439         CodecCapabilities cap = null;
440         try {
441             cap = codec.getCodecInfo().getCapabilitiesForType(mime);
442             return cap.isFormatSupported(format);
443         } catch (IllegalArgumentException e) {
444             Log.w(TAG, "not supported mime: " + mime);
445             return false;
446         } finally {
447             codec.release();
448         }
449     }
450 
hasCodecForTrack(MediaExtractor ex, int track)451     public static boolean hasCodecForTrack(MediaExtractor ex, int track) {
452         int count = ex.getTrackCount();
453         if (track < 0 || track >= count) {
454             throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]");
455         }
456         return canDecode(ex.getTrackFormat(track));
457     }
458 
459     /**
460      * return true iff all audio and video tracks are supported
461      */
hasCodecsForMedia(MediaExtractor ex)462     public static boolean hasCodecsForMedia(MediaExtractor ex) {
463         for (int i = 0; i < ex.getTrackCount(); ++i) {
464             MediaFormat format = ex.getTrackFormat(i);
465             // only check for audio and video codecs
466             String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
467             if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
468                 continue;
469             }
470             if (!canDecode(format)) {
471                 return false;
472             }
473         }
474         return true;
475     }
476 
477     /**
478      * return true iff any track starting with mimePrefix is supported
479      */
hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)480     public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) {
481         mimePrefix = mimePrefix.toLowerCase();
482         for (int i = 0; i < ex.getTrackCount(); ++i) {
483             MediaFormat format = ex.getTrackFormat(i);
484             String mime = format.getString(MediaFormat.KEY_MIME);
485             if (mime.toLowerCase().startsWith(mimePrefix)) {
486                 if (canDecode(format)) {
487                     return true;
488                 }
489                 Log.i(TAG, "no decoder for " + format);
490             }
491         }
492         return false;
493     }
494 
hasCodecsForResourceCombo(final String resource, int track, String mimePrefix)495     private static boolean hasCodecsForResourceCombo(final String resource, int track,
496             String mimePrefix) {
497         try {
498             AssetFileDescriptor afd = null;
499             MediaExtractor ex = null;
500             try {
501                 ex = new MediaExtractor();
502                 ex.setDataSource(resource);
503                 if (mimePrefix != null) {
504                     return hasCodecForMediaAndDomain(ex, mimePrefix);
505                 } else if (track == ALL_AV_TRACKS) {
506                     return hasCodecsForMedia(ex);
507                 } else {
508                     return hasCodecForTrack(ex, track);
509                 }
510             } finally {
511                 if (ex != null) {
512                     ex.release();
513                 }
514                 if (afd != null) {
515                     afd.close();
516                 }
517             }
518         } catch (IOException e) {
519             Log.i(TAG, "could not open resource");
520         }
521         return false;
522     }
523 
hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)524     private static boolean hasCodecsForResourceCombo(
525             Context context, int resourceId, int track, String mimePrefix) {
526         try {
527             AssetFileDescriptor afd = null;
528             MediaExtractor ex = null;
529             try {
530                 afd = context.getResources().openRawResourceFd(resourceId);
531                 ex = new MediaExtractor();
532                 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
533                 if (mimePrefix != null) {
534                     return hasCodecForMediaAndDomain(ex, mimePrefix);
535                 } else if (track == ALL_AV_TRACKS) {
536                     return hasCodecsForMedia(ex);
537                 } else {
538                     return hasCodecForTrack(ex, track);
539                 }
540             } finally {
541                 if (ex != null) {
542                     ex.release();
543                 }
544                 if (afd != null) {
545                     afd.close();
546                 }
547             }
548         } catch (IOException e) {
549             Log.i(TAG, "could not open resource");
550         }
551         return false;
552     }
553 
554     /**
555      * return true iff all audio and video tracks are supported
556      */
hasCodecsForResource(Context context, int resourceId)557     public static boolean hasCodecsForResource(Context context, int resourceId) {
558         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
559     }
560 
checkCodecsForResource(Context context, int resourceId)561     public static boolean checkCodecsForResource(Context context, int resourceId) {
562         return check(hasCodecsForResource(context, resourceId), "no decoder found");
563     }
564 
hasCodecsForResource(final String resource)565     public static boolean hasCodecsForResource(final String resource) {
566         return hasCodecsForResourceCombo(resource, ALL_AV_TRACKS, null /* mimePrefix */);
567     }
568 
checkCodecsForResource(final String resource)569     public static boolean checkCodecsForResource(final String resource) {
570         return check(hasCodecsForResource(resource), "no decoder found");
571     }
572 
573     /**
574      * return true iff track is supported.
575      */
hasCodecForResource(Context context, int resourceId, int track)576     public static boolean hasCodecForResource(Context context, int resourceId, int track) {
577         return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
578     }
579 
checkCodecForResource(Context context, int resourceId, int track)580     public static boolean checkCodecForResource(Context context, int resourceId, int track) {
581         return check(hasCodecForResource(context, resourceId, track), "no decoder found");
582     }
583 
hasCodecForResource(final String resource, int track)584     public static boolean hasCodecForResource(final String resource, int track) {
585         return hasCodecsForResourceCombo(resource, track, null /* mimePrefix */);
586     }
587 
checkCodecForResource(final String resource, int track)588     public static boolean checkCodecForResource(final String resource, int track) {
589         return check(hasCodecForResource(resource, track), "no decoder found");
590     }
591 
592     /**
593      * return true iff any track starting with mimePrefix is supported
594      */
hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)595     public static boolean hasCodecForResourceAndDomain(
596             Context context, int resourceId, String mimePrefix) {
597         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix);
598     }
599 
hasCodecForResourceAndDomain(String resource, String mimePrefix)600     public static boolean hasCodecForResourceAndDomain(String resource, String mimePrefix) {
601         return hasCodecsForResourceCombo(resource, ALL_AV_TRACKS, mimePrefix);
602     }
603 
604     /**
605      * return true iff all audio and video tracks are supported
606      */
hasCodecsForPath(Context context, String path)607     public static boolean hasCodecsForPath(Context context, String path) {
608         MediaExtractor ex = null;
609         try {
610             ex = getExtractorForPath(context, path);
611             return hasCodecsForMedia(ex);
612         } catch (IOException e) {
613             Log.i(TAG, "could not open path " + path);
614         } finally {
615             if (ex != null) {
616                 ex.release();
617             }
618         }
619         return true;
620     }
621 
getExtractorForPath(Context context, String path)622     private static MediaExtractor getExtractorForPath(Context context, String path)
623             throws IOException {
624         Uri uri = Uri.parse(path);
625         String scheme = uri.getScheme();
626         MediaExtractor ex = new MediaExtractor();
627         try {
628             if (scheme == null) { // file
629                 ex.setDataSource(path);
630             } else if (scheme.equalsIgnoreCase("file")) {
631                 ex.setDataSource(uri.getPath());
632             } else {
633                 ex.setDataSource(context, uri, null);
634             }
635         } catch (IOException e) {
636             ex.release();
637             throw e;
638         }
639         return ex;
640     }
641 
checkCodecsForPath(Context context, String path)642     public static boolean checkCodecsForPath(Context context, String path) {
643         return check(hasCodecsForPath(context, path), "no decoder found");
644     }
645 
hasCodecForDomain(boolean encoder, String domain)646     public static boolean hasCodecForDomain(boolean encoder, String domain) {
647         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
648             if (encoder != info.isEncoder()) {
649                 continue;
650             }
651 
652             for (String type : info.getSupportedTypes()) {
653                 if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) {
654                     Log.i(TAG, "found codec " + info.getName() + " for mime " + type);
655                     return true;
656                 }
657             }
658         }
659         return false;
660     }
661 
checkCodecForDomain(boolean encoder, String domain)662     public static boolean checkCodecForDomain(boolean encoder, String domain) {
663         return check(hasCodecForDomain(encoder, domain),
664                 "no " + domain + (encoder ? " encoder" : " decoder") + " found");
665     }
666 
hasCodecForMime(boolean encoder, String mime)667     private static boolean hasCodecForMime(boolean encoder, String mime) {
668         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
669             if (encoder != info.isEncoder()) {
670                 continue;
671             }
672 
673             for (String type : info.getSupportedTypes()) {
674                 if (type.equalsIgnoreCase(mime)) {
675                     Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
676                     return true;
677                 }
678             }
679         }
680         return false;
681     }
682 
hasCodecForMimes(boolean encoder, String[] mimes)683     private static boolean hasCodecForMimes(boolean encoder, String[] mimes) {
684         for (String mime : mimes) {
685             if (!hasCodecForMime(encoder, mime)) {
686                 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime);
687                 return false;
688             }
689         }
690         return true;
691     }
692 
693 
hasEncoder(String... mimes)694     public static boolean hasEncoder(String... mimes) {
695         return hasCodecForMimes(true /* encoder */, mimes);
696     }
697 
hasDecoder(String... mimes)698     public static boolean hasDecoder(String... mimes) {
699         return hasCodecForMimes(false /* encoder */, mimes);
700     }
701 
checkDecoder(String... mimes)702     public static boolean checkDecoder(String... mimes) {
703         return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
704     }
705 
checkEncoder(String... mimes)706     public static boolean checkEncoder(String... mimes) {
707         return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
708     }
709 
710     // checks format, does not address actual speed of decoding
canDecodeVideo(String mime, int width, int height, float rate)711     public static boolean canDecodeVideo(String mime, int width, int height, float rate) {
712         return canDecodeVideo(mime, width, height, rate, (float)0.0);
713     }
714 
715     // format + decode rate
canDecodeVideo(String mime, int width, int height, float rate, float decodeRate)716     public static boolean canDecodeVideo(String mime, int width, int height, float rate, float decodeRate) {
717         MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
718         format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
719         return canDecode(format, decodeRate);
720     }
721 
canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate)722     public static boolean canDecodeVideo(
723             String mime, int width, int height, float rate,
724             Integer profile, Integer level, Integer bitrate) {
725         return canDecodeVideo(mime, width, height, rate, profile, level, bitrate, (float)0.0);
726     }
727 
canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate, float decodeRate)728     public static boolean canDecodeVideo(
729             String mime, int width, int height, float rate,
730             Integer profile, Integer level, Integer bitrate, float decodeRate) {
731         MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
732         format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
733         if (profile != null) {
734             format.setInteger(MediaFormat.KEY_PROFILE, profile);
735             if (level != null) {
736                 format.setInteger(MediaFormat.KEY_LEVEL, level);
737             }
738         }
739         if (bitrate != null) {
740             format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
741         }
742         return canDecode(format, decodeRate);
743     }
744 
checkEncoderForFormat(MediaFormat format)745     public static boolean checkEncoderForFormat(MediaFormat format) {
746         return check(canEncode(format), "no encoder for " + format);
747     }
748 
checkDecoderForFormat(MediaFormat format)749     public static boolean checkDecoderForFormat(MediaFormat format) {
750         return check(canDecode(format), "no decoder for " + format);
751     }
752 
753     /*
754      *  ----------------------- HELPER METHODS FOR MEDIA HANDLING -----------------------
755      */
756 
getVideoCapabilities(String codecName, String mime)757     public static VideoCapabilities getVideoCapabilities(String codecName, String mime) {
758         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
759             if (!info.getName().equalsIgnoreCase(codecName)) {
760                 continue;
761             }
762             CodecCapabilities caps;
763             try {
764                 caps = info.getCapabilitiesForType(mime);
765             } catch (IllegalArgumentException e) {
766                 // mime is not supported
767                 Log.w(TAG, "not supported mime: " + mime);
768                 return null;
769             }
770             VideoCapabilities vidCaps = caps.getVideoCapabilities();
771             if (vidCaps == null) {
772                 Log.w(TAG, "not a video codec: " + codecName);
773             }
774             return vidCaps;
775         }
776         Log.w(TAG, "codec not found: " + codecName);
777         return null;
778     }
779 
getTrackFormatForResource( Context context, int resourceId, String mimeTypePrefix)780     public static MediaFormat getTrackFormatForResource(
781             Context context,
782             int resourceId,
783             String mimeTypePrefix) throws IOException {
784         MediaExtractor extractor = new MediaExtractor();
785         AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
786         try {
787             extractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
788         } finally {
789             afd.close();
790         }
791         return getTrackFormatForExtractor(extractor, mimeTypePrefix);
792     }
793 
getTrackFormatForResource( final String resource, String mimeTypePrefix)794     public static MediaFormat getTrackFormatForResource(
795             final String resource,
796             String mimeTypePrefix) throws IOException {
797         MediaExtractor extractor = new MediaExtractor();
798         try {
799             extractor.setDataSource(resource);
800         } catch (IOException e) {
801             e.printStackTrace();
802         }
803         return getTrackFormatForExtractor(extractor, mimeTypePrefix);
804     }
805 
getTrackFormatForPath( Context context, String path, String mimeTypePrefix)806     public static MediaFormat getTrackFormatForPath(
807             Context context, String path, String mimeTypePrefix)
808             throws IOException {
809       MediaExtractor extractor = getExtractorForPath(context, path);
810       return getTrackFormatForExtractor(extractor, mimeTypePrefix);
811     }
812 
getTrackFormatForExtractor( MediaExtractor extractor, String mimeTypePrefix)813     private static MediaFormat getTrackFormatForExtractor(
814             MediaExtractor extractor,
815             String mimeTypePrefix) {
816       int trackIndex;
817       MediaFormat format = null;
818       for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
819           MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
820           if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
821               format = trackMediaFormat;
822               break;
823           }
824       }
825       extractor.release();
826       if (format == null) {
827           throw new RuntimeException("couldn't get a track for " + mimeTypePrefix);
828       }
829 
830       return format;
831     }
832 
createMediaExtractorForMimeType( Context context, String resource, String mimeTypePrefix)833     public static MediaExtractor createMediaExtractorForMimeType(
834             Context context, String resource, String mimeTypePrefix)
835             throws IOException {
836         MediaExtractor extractor = new MediaExtractor();
837         File inpFile = new File(resource);
838         ParcelFileDescriptor parcelFD =
839                 ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY);
840         AssetFileDescriptor afd = new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
841         try {
842             extractor.setDataSource(
843                     afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
844         } finally {
845             afd.close();
846         }
847         int trackIndex;
848         for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
849             MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
850             if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
851                 extractor.selectTrack(trackIndex);
852                 break;
853             }
854         }
855         if (trackIndex == extractor.getTrackCount()) {
856             extractor.release();
857             throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix);
858         }
859 
860         return extractor;
861     }
862 
863     /*
864      *  ---------------------- HELPER METHODS FOR CODEC CONFIGURATION
865      */
866 
867     /** Format must contain mime, width and height.
868      *  Throws Exception if encoder does not support this width and height */
setMaxEncoderFrameAndBitrates( MediaCodec encoder, MediaFormat format, int maxFps)869     public static void setMaxEncoderFrameAndBitrates(
870             MediaCodec encoder, MediaFormat format, int maxFps) {
871         String mime = format.getString(MediaFormat.KEY_MIME);
872 
873         VideoCapabilities vidCaps =
874             encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities();
875         setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps);
876     }
877 
setMaxEncoderFrameAndBitrates( VideoCapabilities vidCaps, MediaFormat format, int maxFps)878     public static void setMaxEncoderFrameAndBitrates(
879             VideoCapabilities vidCaps, MediaFormat format, int maxFps) {
880         int width = format.getInteger(MediaFormat.KEY_WIDTH);
881         int height = format.getInteger(MediaFormat.KEY_HEIGHT);
882 
883         int maxWidth = vidCaps.getSupportedWidths().getUpper();
884         int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper();
885         int frameRate = Math.min(
886                 maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue());
887         format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
888 
889         int bitrate = vidCaps.getBitrateRange().clamp(
890             (int)(vidCaps.getBitrateRange().getUpper() /
891                   Math.sqrt((double)maxWidth * maxHeight / width / height)));
892         format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
893     }
894 
hasHardwareCodec(String mime, boolean encode)895     public static boolean hasHardwareCodec(String mime, boolean encode) {
896         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
897             if (info.isEncoder() == encode && info.isHardwareAccelerated()) {
898                 try {
899                      if (info.getCapabilitiesForType(mime) != null) {
900                          return true;
901                      }
902                 } catch (IllegalArgumentException e) {
903                      // mime is not supported
904                      Log.w(TAG, "not supported mime: " + mime);
905                      return false;
906                 }
907             }
908         }
909         return false;
910     }
911 
912     /*
913      *  ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------
914      */
915 
916     // TODO: migrate this into com.android.compatibility.common.util.Stat
917     public static class Stats {
918         /** does not support NaN or Inf in |data| */
Stats(double[] data)919         public Stats(double[] data) {
920             mData = data;
921             if (mData != null) {
922                 mNum = mData.length;
923             }
924         }
925 
getNum()926         public int getNum() {
927             return mNum;
928         }
929 
930         /** calculate mSumX and mSumXX */
analyze()931         private void analyze() {
932             if (mAnalyzed) {
933                 return;
934             }
935 
936             if (mData != null) {
937                 for (double x : mData) {
938                     if (!(x >= mMinX)) { // mMinX may be NaN
939                         mMinX = x;
940                     }
941                     if (!(x <= mMaxX)) { // mMaxX may be NaN
942                         mMaxX = x;
943                     }
944                     mSumX += x;
945                     mSumXX += x * x;
946                 }
947             }
948             mAnalyzed = true;
949         }
950 
951         /** returns the maximum or NaN if it does not exist */
getMin()952         public double getMin() {
953             analyze();
954             return mMinX;
955         }
956 
957         /** returns the minimum or NaN if it does not exist */
getMax()958         public double getMax() {
959             analyze();
960             return mMaxX;
961         }
962 
963         /** returns the average or NaN if it does not exist. */
getAverage()964         public double getAverage() {
965             analyze();
966             if (mNum == 0) {
967                 return Double.NaN;
968             } else {
969                 return mSumX / mNum;
970             }
971         }
972 
973         /** returns the standard deviation or NaN if it does not exist. */
getStdev()974         public double getStdev() {
975             analyze();
976             if (mNum == 0) {
977                 return Double.NaN;
978             } else {
979                 double average = mSumX / mNum;
980                 return Math.sqrt(mSumXX / mNum - average * average);
981             }
982         }
983 
984         /** returns the statistics for the moving average over n values */
movingAverage(int n)985         public Stats movingAverage(int n) {
986             if (n < 1 || mNum < n) {
987                 return new Stats(null);
988             } else if (n == 1) {
989                 return this;
990             }
991 
992             double[] avgs = new double[mNum - n + 1];
993             double sum = 0;
994             for (int i = 0; i < mNum; ++i) {
995                 sum += mData[i];
996                 if (i >= n - 1) {
997                     avgs[i - n + 1] = sum / n;
998                     sum -= mData[i - n + 1];
999                 }
1000             }
1001             return new Stats(avgs);
1002         }
1003 
1004         /** returns the statistics for the moving average over a window over the
1005          *  cumulative sum. Basically, moves a window from: [0, window] to
1006          *  [sum - window, sum] over the cumulative sum, over ((sum - window) / average)
1007          *  steps, and returns the average value over each window.
1008          *  This method is used to average time-diff data over a window of a constant time.
1009          */
movingAverageOverSum(double window)1010         public Stats movingAverageOverSum(double window) {
1011             if (window <= 0 || mNum < 1) {
1012                 return new Stats(null);
1013             }
1014 
1015             analyze();
1016             double average = mSumX / mNum;
1017             if (window >= mSumX) {
1018                 return new Stats(new double[] { average });
1019             }
1020             int samples = (int)Math.ceil((mSumX - window) / average);
1021             double[] avgs = new double[samples];
1022 
1023             // A somewhat brute force approach to calculating the moving average.
1024             // TODO: add support for weights in Stats, so we can do a more refined approach.
1025             double sum = 0; // sum of elements in the window
1026             int num = 0; // number of elements in the moving window
1027             int bi = 0; // index of the first element in the moving window
1028             int ei = 0; // index of the last element in the moving window
1029             double space = window; // space at the end of the window
1030             double foot = 0; // space at the beginning of the window
1031 
1032             // invariants: foot + sum + space == window
1033             //             bi + num == ei
1034             //
1035             //  window:             |-------------------------------|
1036             //                      |    <-----sum------>           |
1037             //                      <foot>               <---space-->
1038             //                           |               |
1039             //  intervals:   |-----------|-------|-------|--------------------|--------|
1040             //                           ^bi             ^ei
1041 
1042             int ix = 0; // index in the result
1043             while (ix < samples) {
1044                 // add intervals while there is space in the window
1045                 while (ei < mData.length && mData[ei] <= space) {
1046                     space -= mData[ei];
1047                     sum += mData[ei];
1048                     num++;
1049                     ei++;
1050                 }
1051 
1052                 // calculate average over window and deal with odds and ends (e.g. if there are no
1053                 // intervals in the current window: pick whichever element overlaps the window
1054                 // most.
1055                 if (num > 0) {
1056                     avgs[ix++] = sum / num;
1057                 } else if (bi > 0 && foot > space) {
1058                     // consider previous
1059                     avgs[ix++] = mData[bi - 1];
1060                 } else if (ei == mData.length) {
1061                     break;
1062                 } else {
1063                     avgs[ix++] = mData[ei];
1064                 }
1065 
1066                 // move the window to the next position
1067                 foot -= average;
1068                 space += average;
1069 
1070                 // remove intervals that are now partially or wholly outside of the window
1071                 while (bi < ei && foot < 0) {
1072                     foot += mData[bi];
1073                     sum -= mData[bi];
1074                     num--;
1075                     bi++;
1076                 }
1077             }
1078             return new Stats(Arrays.copyOf(avgs, ix));
1079         }
1080 
1081         /** calculate mSortedData */
sort()1082         private void sort() {
1083             if (mSorted || mNum == 0) {
1084                 return;
1085             }
1086             mSortedData = Arrays.copyOf(mData, mNum);
1087             Arrays.sort(mSortedData);
1088             mSorted = true;
1089         }
1090 
1091         /** returns an array of percentiles for the points using nearest rank */
getPercentiles(double... points)1092         public double[] getPercentiles(double... points) {
1093             sort();
1094             double[] res = new double[points.length];
1095             for (int i = 0; i < points.length; ++i) {
1096                 if (mNum < 1 || points[i] < 0 || points[i] > 100) {
1097                     res[i] = Double.NaN;
1098                 } else {
1099                     res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))];
1100                 }
1101             }
1102             return res;
1103         }
1104 
1105         @Override
equals(Object o)1106         public boolean equals(Object o) {
1107             if (o instanceof Stats) {
1108                 Stats other = (Stats)o;
1109                 if (other.mNum != mNum) {
1110                     return false;
1111                 } else if (mNum == 0) {
1112                     return true;
1113                 }
1114                 return Arrays.equals(mData, other.mData);
1115             }
1116             return false;
1117         }
1118 
1119         private double[] mData;
1120         private double mSumX = 0;
1121         private double mSumXX = 0;
1122         private double mMinX = Double.NaN;
1123         private double mMaxX = Double.NaN;
1124         private int mNum = 0;
1125         private boolean mAnalyzed = false;
1126         private double[] mSortedData;
1127         private boolean mSorted = false;
1128     }
1129 
1130     /**
1131      * Convert a forward lock .dm message stream to a .fl file
1132      * @param context Context to use
1133      * @param dmStream The .dm message
1134      * @param flFile The output file to be written
1135      * @return success
1136      */
convertDmToFl( Context context, InputStream dmStream, RandomAccessFile flFile)1137     public static boolean convertDmToFl(
1138             Context context,
1139             InputStream dmStream,
1140             RandomAccessFile flFile) {
1141         final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message";
1142         byte[] dmData = new byte[10000];
1143         int totalRead = 0;
1144         int numRead;
1145         while (true) {
1146             try {
1147                 numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead);
1148             } catch (IOException e) {
1149                 Log.w(TAG, "Failed to read from input file");
1150                 return false;
1151             }
1152             if (numRead == -1) {
1153                 break;
1154             }
1155             totalRead += numRead;
1156             if (totalRead == dmData.length) {
1157                 // grow array
1158                 dmData = Arrays.copyOf(dmData, dmData.length + 10000);
1159             }
1160         }
1161         byte[] fileData = Arrays.copyOf(dmData, totalRead);
1162 
1163         DrmManagerClient drmClient = null;
1164         try {
1165             drmClient = new DrmManagerClient(context);
1166         } catch (IllegalArgumentException e) {
1167             Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal.");
1168             return false;
1169         } catch (IllegalStateException e) {
1170             Log.w(TAG, "DrmManagerClient didn't initialize properly.");
1171             return false;
1172         }
1173 
1174         try {
1175             int convertSessionId = -1;
1176             try {
1177                 convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE);
1178             } catch (IllegalArgumentException e) {
1179                 Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE
1180                         + " is not supported.", e);
1181                 return false;
1182             } catch (IllegalStateException e) {
1183                 Log.w(TAG, "Could not access Open DrmFramework.", e);
1184                 return false;
1185             }
1186 
1187             if (convertSessionId < 0) {
1188                 Log.w(TAG, "Failed to open session.");
1189                 return false;
1190             }
1191 
1192             DrmConvertedStatus convertedStatus = null;
1193             try {
1194                 convertedStatus = drmClient.convertData(convertSessionId, fileData);
1195             } catch (IllegalArgumentException e) {
1196                 Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: "
1197                         + convertSessionId, e);
1198                 return false;
1199             } catch (IllegalStateException e) {
1200                 Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e);
1201                 return false;
1202             }
1203 
1204             if (convertedStatus == null ||
1205                     convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
1206                     convertedStatus.convertedData == null) {
1207                 Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId);
1208                 try {
1209                     DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId);
1210                     if (result.statusCode != DrmConvertedStatus.STATUS_OK) {
1211                         Log.w(TAG, "Conversion failed with status: " + result.statusCode);
1212                         return false;
1213                     }
1214                 } catch (IllegalStateException e) {
1215                     Log.w(TAG, "Could not close session. Convertsession: " +
1216                            convertSessionId, e);
1217                 }
1218                 return false;
1219             }
1220 
1221             try {
1222                 flFile.write(convertedStatus.convertedData, 0, convertedStatus.convertedData.length);
1223             } catch (IOException e) {
1224                 Log.w(TAG, "Failed to write to output file: " + e);
1225                 return false;
1226             }
1227 
1228             try {
1229                 convertedStatus = drmClient.closeConvertSession(convertSessionId);
1230             } catch (IllegalStateException e) {
1231                 Log.w(TAG, "Could not close convertsession. Convertsession: " +
1232                         convertSessionId, e);
1233                 return false;
1234             }
1235 
1236             if (convertedStatus == null ||
1237                     convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
1238                     convertedStatus.convertedData == null) {
1239                 Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId);
1240                 return false;
1241             }
1242 
1243             try {
1244                 flFile.seek(convertedStatus.offset);
1245                 flFile.write(convertedStatus.convertedData);
1246             } catch (IOException e) {
1247                 Log.w(TAG, "Could not update file.", e);
1248                 return false;
1249             }
1250 
1251             return true;
1252         } finally {
1253             drmClient.close();
1254         }
1255     }
1256 
1257     /**
1258      * @param decoder new MediaCodec object
1259      * @param ex MediaExtractor after setDataSource and selectTrack
1260      * @param frameMD5Sums reference MD5 checksum for decoded frames
1261      * @return true if decoded frames checksums matches reference checksums
1262      * @throws IOException
1263      */
verifyDecoder( MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)1264     public static boolean verifyDecoder(
1265             MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)
1266             throws IOException {
1267 
1268         int trackIndex = ex.getSampleTrackIndex();
1269         MediaFormat format = ex.getTrackFormat(trackIndex);
1270         decoder.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
1271         decoder.start();
1272 
1273         boolean sawInputEOS = false;
1274         boolean sawOutputEOS = false;
1275         final long kTimeOutUs = 5000; // 5ms timeout
1276         int decodedFrameCount = 0;
1277         int expectedFrameCount = frameMD5Sums.size();
1278         MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
1279 
1280         while (!sawOutputEOS) {
1281             // handle input
1282             if (!sawInputEOS) {
1283                 int inIdx = decoder.dequeueInputBuffer(kTimeOutUs);
1284                 if (inIdx >= 0) {
1285                     ByteBuffer buffer = decoder.getInputBuffer(inIdx);
1286                     int sampleSize = ex.readSampleData(buffer, 0);
1287                     if (sampleSize < 0) {
1288                         final int flagEOS = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
1289                         decoder.queueInputBuffer(inIdx, 0, 0, 0, flagEOS);
1290                         sawInputEOS = true;
1291                     } else {
1292                         decoder.queueInputBuffer(inIdx, 0, sampleSize, ex.getSampleTime(), 0);
1293                         ex.advance();
1294                     }
1295                 }
1296             }
1297 
1298             // handle output
1299             int outputBufIndex = decoder.dequeueOutputBuffer(info, kTimeOutUs);
1300             if (outputBufIndex >= 0) {
1301                 try {
1302                     if (info.size > 0) {
1303                         // Disregard 0-sized buffers at the end.
1304                         String md5CheckSum = "";
1305                         Image image = decoder.getOutputImage(outputBufIndex);
1306                         md5CheckSum = getImageMD5Checksum(image);
1307 
1308                         if (!md5CheckSum.equals(frameMD5Sums.get(decodedFrameCount))) {
1309                             Log.d(TAG,
1310                                     String.format(
1311                                             "Frame %d md5sum mismatch: %s(actual) vs %s(expected)",
1312                                             decodedFrameCount, md5CheckSum,
1313                                             frameMD5Sums.get(decodedFrameCount)));
1314                             return false;
1315                         }
1316 
1317                         decodedFrameCount++;
1318                     }
1319                 } catch (Exception e) {
1320                     Log.e(TAG, "getOutputImage md5CheckSum failed", e);
1321                     return false;
1322                 } finally {
1323                     decoder.releaseOutputBuffer(outputBufIndex, false /* render */);
1324                 }
1325                 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
1326                     sawOutputEOS = true;
1327                 }
1328             } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
1329                 MediaFormat decOutputFormat = decoder.getOutputFormat();
1330                 Log.d(TAG, "output format " + decOutputFormat);
1331             } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
1332                 Log.i(TAG, "Skip handling MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
1333             } else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
1334                 continue;
1335             } else {
1336                 Log.w(TAG, "decoder.dequeueOutputBuffer() unrecognized index: " + outputBufIndex);
1337                 return false;
1338             }
1339         }
1340 
1341         if (decodedFrameCount != expectedFrameCount) {
1342             return false;
1343         }
1344 
1345         return true;
1346     }
1347 
getImageMD5Checksum(Image image)1348     public static String getImageMD5Checksum(Image image) throws Exception {
1349         int format = image.getFormat();
1350         if (ImageFormat.YUV_420_888 != format) {
1351             Log.w(TAG, "unsupported image format");
1352             return "";
1353         }
1354 
1355         MessageDigest md = MessageDigest.getInstance("MD5");
1356 
1357         Rect crop = image.getCropRect();
1358         int cropLeft = crop.left;
1359         int cropRight = crop.right;
1360         int cropTop = crop.top;
1361         int cropBottom = crop.bottom;
1362 
1363         int imageWidth = cropRight - cropLeft;
1364         int imageHeight = cropBottom - cropTop;
1365 
1366         Image.Plane[] planes = image.getPlanes();
1367         for (int i = 0; i < planes.length; ++i) {
1368             ByteBuffer buf = planes[i].getBuffer();
1369 
1370             int width, height, rowStride, pixelStride, x, y, top, left;
1371             rowStride = planes[i].getRowStride();
1372             pixelStride = planes[i].getPixelStride();
1373             if (i == 0) {
1374                 width = imageWidth;
1375                 height = imageHeight;
1376                 left = cropLeft;
1377                 top = cropTop;
1378             } else {
1379                 width = imageWidth / 2;
1380                 height = imageHeight /2;
1381                 left = cropLeft / 2;
1382                 top = cropTop / 2;
1383             }
1384             // local contiguous pixel buffer
1385             byte[] bb = new byte[width * height];
1386             if (buf.hasArray()) {
1387                 byte b[] = buf.array();
1388                 int offs = buf.arrayOffset() + left * pixelStride;
1389                 if (pixelStride == 1) {
1390                     for (y = 0; y < height; ++y) {
1391                         System.arraycopy(bb, y * width, b, (top + y) * rowStride + offs, width);
1392                     }
1393                 } else {
1394                     // do it pixel-by-pixel
1395                     for (y = 0; y < height; ++y) {
1396                         int lineOffset = offs + (top + y) * rowStride;
1397                         for (x = 0; x < width; ++x) {
1398                             bb[y * width + x] = b[lineOffset + x * pixelStride];
1399                         }
1400                     }
1401                 }
1402             } else { // almost always ends up here due to direct buffers
1403                 int pos = buf.position();
1404                 if (pixelStride == 1) {
1405                     for (y = 0; y < height; ++y) {
1406                         buf.position(pos + left + (top + y) * rowStride);
1407                         buf.get(bb, y * width, width);
1408                     }
1409                 } else {
1410                     // local line buffer
1411                     byte[] lb = new byte[rowStride];
1412                     // do it pixel-by-pixel
1413                     for (y = 0; y < height; ++y) {
1414                         buf.position(pos + left * pixelStride + (top + y) * rowStride);
1415                         // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes
1416                         buf.get(lb, 0, pixelStride * (width - 1) + 1);
1417                         for (x = 0; x < width; ++x) {
1418                             bb[y * width + x] = lb[x * pixelStride];
1419                         }
1420                     }
1421                 }
1422                 buf.position(pos);
1423             }
1424             md.update(bb, 0, width * height);
1425         }
1426 
1427         return convertByteArrayToHEXString(md.digest());
1428     }
1429 
convertByteArrayToHEXString(byte[] ba)1430     private static String convertByteArrayToHEXString(byte[] ba) throws Exception {
1431         StringBuilder result = new StringBuilder();
1432         for (int i = 0; i < ba.length; i++) {
1433             result.append(Integer.toString((ba[i] & 0xff) + 0x100, 16).substring(1));
1434         }
1435         return result.toString();
1436     }
1437 
1438 
1439     /*
1440      *  ------------------- HELPER METHODS FOR DETECTING DEVICE TYPES -------------------
1441      */
1442 
hasDeviceGotBattery()1443     public static boolean hasDeviceGotBattery() {
1444         final Intent batteryInfo = mContext.registerReceiver(null,
1445                 new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
1446         return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
1447     }
1448 
getScreenSizeInInches()1449     public static double getScreenSizeInInches() {
1450         DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
1451         double widthInInchesSquared = Math.pow(dm.widthPixels/dm.xdpi,2);
1452         double heightInInchesSquared = Math.pow(dm.heightPixels/dm.ydpi,2);
1453         double diagonalInInches = Math.sqrt(widthInInchesSquared + heightInInchesSquared);
1454         return diagonalInInches;
1455     }
1456 
isTv()1457     public static boolean isTv() {
1458         return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
1459                 pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION);
1460     }
1461 
hasMicrophone()1462     public static boolean hasMicrophone() {
1463         return pm.hasSystemFeature(PackageManager.FEATURE_MICROPHONE);
1464     }
1465 
hasCamera()1466     public static boolean hasCamera() {
1467         return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
1468     }
1469 
isWatch()1470     public static boolean isWatch() {
1471         return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
1472     }
1473 
isAutomotive()1474     public static boolean isAutomotive() {
1475         return pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
1476     }
1477 
isPc()1478     public static boolean isPc() {
1479         return pm.hasSystemFeature(PackageManager.FEATURE_PC);
1480     }
1481 
hasAudioOutput()1482     public static boolean hasAudioOutput() {
1483         return pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT);
1484     }
1485 
isHandheld()1486     public static boolean isHandheld() {
1487         double screenSize = getScreenSizeInInches();
1488         if (screenSize < (FIRST_SDK_IS_AT_LEAST_R ? 3.3 : 2.5)) return false;
1489         if (screenSize > 8.0) return false;
1490         if (!hasDeviceGotBattery()) return false;
1491         // handheld nature is not exposed to package manager, so for now,
1492         // in addition to physical screen size, the following checks are
1493         // also required:
1494         if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false;
1495         if (isWatch()) return false;
1496         if (isTv()) return false;
1497         if (isAutomotive()) return false;
1498         if (isPc()) return false;
1499         return true;
1500     }
1501 
isTablet()1502     public static boolean isTablet() {
1503         double screenSize = getScreenSizeInInches();
1504         if (screenSize < 7.0) return false;
1505         if (screenSize > 18.0) return false;
1506         if (!hasDeviceGotBattery()) return false;
1507         // tablet nature is not exposed to package manager, so for now,
1508         // in addition to physical screen size, the following checks are
1509         // also required:
1510         if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false;
1511         if (isWatch()) return false;
1512         if (isTv()) return false;
1513         if (isAutomotive()) return false;
1514         if (isPc()) return false;
1515         return true;
1516     }
1517 
1518     /*
1519      *  ------------------- HELPER METHODS FOR DETECTING NON-PRODUCTION DEVICES -------------------
1520      */
1521 
1522     /*
1523      *  Some parts of media CTS verifies device characterization that does not make sense for
1524      *  non-production devices (such as GSI and cuttlefish). We call these devices 'frankenDevices'.
1525      *  We may also limit test duration on these devices.
1526      */
onFrankenDevice()1527     public static boolean onFrankenDevice() throws IOException {
1528         String systemBrand = PropertyUtil.getProperty("ro.product.system.brand");
1529         String systemModel = PropertyUtil.getProperty("ro.product.system.model");
1530         String systemProduct = PropertyUtil.getProperty("ro.product.system.name");
1531 
1532         // not all devices may have system_ext partition, but if they do use that
1533         {
1534             String systemExtProduct = PropertyUtil.getProperty("ro.product.system_ext.name");
1535             if (systemExtProduct != null) {
1536                 systemProduct = systemExtProduct;
1537             }
1538             String systemExtModel = PropertyUtil.getProperty("ro.product.system_ext.model");
1539             if (systemExtModel != null) {
1540                 systemModel = systemExtModel;
1541             }
1542         }
1543 
1544         if (("Android".equals(systemBrand) || "generic".equals(systemBrand) ||
1545                 "mainline".equals(systemBrand)) &&
1546             (systemModel.startsWith("AOSP on ") || systemProduct.startsWith("aosp_") ||
1547                 systemModel.startsWith("GSI on ") || systemProduct.startsWith("gsi_"))) {
1548             return true;
1549         }
1550 
1551         // Return true for cuttlefish instances
1552         if ((systemBrand.equals("Android") || systemBrand.equals("google")) &&
1553                 (systemProduct.startsWith("cf_") || systemProduct.startsWith("aosp_cf_") ||
1554                         systemModel.startsWith("Cuttlefish "))) {
1555             return true;
1556         }
1557         return false;
1558     }
1559 
1560     /**
1561      *  Function to identify if the device is a cuttlefish instance
1562      */
onCuttlefish()1563     public static boolean onCuttlefish() throws IOException {
1564         String device = SystemProperties.get("ro.product.device", "");
1565         String model = SystemProperties.get("ro.product.model", "");
1566         String name = SystemProperties.get("ro.product.name", "");
1567 
1568         // Return true for cuttlefish instances
1569         if (!device.startsWith("vsoc_")) {
1570             return false;
1571         }
1572         if (!model.startsWith("Cuttlefish ")) {
1573             return false;
1574         }
1575         if (name.startsWith("cf_") || name.startsWith("aosp_cf_")) {
1576             return true;
1577         }
1578         return false;
1579     }
1580 
1581     /*
1582      *  -------------------------------------- END --------------------------------------
1583      */
1584 }
1585