/* * Copyright 2012 Kevin Gaudin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.acra.collector; import android.annotation.TargetApi; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.SparseArray; import org.acra.ACRA; import org.acra.ACRAConstants; import org.acra.ReportField; import org.acra.builder.ReportBuilder; import org.acra.model.ComplexElement; import org.acra.model.Element; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Set; /** * Collects data about available codecs on the device through the MediaCodecList * API introduced in Android 4.1 JellyBean. * * @author Kevin Gaudin & F43nd1r */ final class MediaCodecListCollector extends Collector { private enum CodecType { AVC, H263, MPEG4, AAC } private static final String COLOR_FORMAT_PREFIX = "COLOR_"; private static final String[] MPEG4_TYPES = {"mp4", "mpeg4", "MP4", "MPEG4"}; private static final String[] AVC_TYPES = {"avc", "h264", "AVC", "H264"}; private static final String[] H263_TYPES = {"h263", "H263"}; private static final String[] AAC_TYPES = {"aac", "AAC"}; private final SparseArray<String> mColorFormatValues = new SparseArray<String>(); private final SparseArray<String> mAVCLevelValues = new SparseArray<String>(); private final SparseArray<String> mAVCProfileValues = new SparseArray<String>(); private final SparseArray<String> mH263LevelValues = new SparseArray<String>(); private final SparseArray<String> mH263ProfileValues = new SparseArray<String>(); private final SparseArray<String> mMPEG4LevelValues = new SparseArray<String>(); private final SparseArray<String> mMPEG4ProfileValues = new SparseArray<String>(); private final SparseArray<String> mAACProfileValues = new SparseArray<String>(); MediaCodecListCollector() { super(ReportField.MEDIA_CODEC_LIST); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @NonNull @Override Element collect(ReportField reportField, ReportBuilder reportBuilder) { try { return collectMediaCodecList(); } catch (JSONException e) { ACRA.log.w("Could not collect media codecs", e); return ACRAConstants.NOT_AVAILABLE; } } @Override boolean shouldCollect(Set<ReportField> crashReportFields, ReportField collect, ReportBuilder reportBuilder) { return super.shouldCollect(crashReportFields, collect, reportBuilder) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; } /** * use reflection to prepare field arrays. */ private void prepare() { try { final Class<?> codecCapabilitiesClass = Class.forName("android.media.MediaCodecInfo$CodecCapabilities"); // Retrieve list of possible Color Format for (Field f : codecCapabilitiesClass.getFields()) { if (Modifier.isStatic(f.getModifiers()) && Modifier.isFinal(f.getModifiers()) && f.getName().startsWith(COLOR_FORMAT_PREFIX)) { mColorFormatValues.put(f.getInt(null), f.getName()); } } // Retrieve lists of possible codecs profiles and levels final Class<?> codecProfileLevelClass = Class.forName("android.media.MediaCodecInfo$CodecProfileLevel"); for (Field f : codecProfileLevelClass.getFields()) { if (Modifier.isStatic(f.getModifiers()) && Modifier.isFinal(f.getModifiers())) { if (f.getName().startsWith("AVCLevel")) { mAVCLevelValues.put(f.getInt(null), f.getName()); } else if (f.getName().startsWith("AVCProfile")) { mAVCProfileValues.put(f.getInt(null), f.getName()); } else if (f.getName().startsWith("H263Level")) { mH263LevelValues.put(f.getInt(null), f.getName()); } else if (f.getName().startsWith("H263Profile")) { mH263ProfileValues.put(f.getInt(null), f.getName()); } else if (f.getName().startsWith("MPEG4Level")) { mMPEG4LevelValues.put(f.getInt(null), f.getName()); } else if (f.getName().startsWith("MPEG4Profile")) { mMPEG4ProfileValues.put(f.getInt(null), f.getName()); } else if (f.getName().startsWith("AAC")) { mAACProfileValues.put(f.getInt(null), f.getName()); } } } } catch (@NonNull ClassNotFoundException ignored) { // NOOP } catch (@NonNull SecurityException ignored) { // NOOP } catch (@NonNull IllegalAccessException ignored) { // NOOP } catch (@NonNull IllegalArgumentException ignored) { // NOOP } } /** * Builds an Element describing the list of available codecs on the device * with their capabilities (supported Color Formats, Codec Profiles et * Levels). * * @return The media codecs information */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @NonNull private Element collectMediaCodecList() throws JSONException { prepare(); final MediaCodecInfo[] infos; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { //noinspection deprecation final int codecCount = MediaCodecList.getCodecCount(); infos = new MediaCodecInfo[codecCount]; for (int codecIdx = 0; codecIdx < codecCount; codecIdx++) { //noinspection deprecation infos[codecIdx] = MediaCodecList.getCodecInfoAt(codecIdx); } } else { infos = new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos(); } final ComplexElement result = new ComplexElement(); for (int i = 0; i < infos.length; i++) { final MediaCodecInfo codecInfo = infos[i]; final JSONObject codec = new JSONObject(); final String[] supportedTypes = codecInfo.getSupportedTypes(); codec.put("name", codecInfo.getName()) .put("isEncoder", codecInfo.isEncoder()); final JSONObject supportedTypesJson = new JSONObject(); for (String type : supportedTypes) { supportedTypesJson.put(type, collectCapabilitiesForType(codecInfo, type)); } codec.put("supportedTypes", supportedTypesJson); result.put(String.valueOf(i), codec); } return result; } /** * Retrieve capabilities (ColorFormats and CodecProfileLevels) for a * specific codec type. * * @param codecInfo the currently inspected codec * @param type supported type to collect * @return the color formats and codec profile levels * available for a specific codec type. */ @NonNull @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private JSONObject collectCapabilitiesForType(@NonNull final MediaCodecInfo codecInfo, @NonNull String type) throws JSONException { final JSONObject result = new JSONObject(); final MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type); // Color Formats final int[] colorFormats = codecCapabilities.colorFormats; if (colorFormats.length > 0) { final JSONArray colorFormatsJson = new JSONArray(); for (int colorFormat : colorFormats) { colorFormatsJson.put(mColorFormatValues.get(colorFormat)); } result.put("colorFormats", colorFormatsJson); } final CodecType codecType = identifyCodecType(codecInfo); // Profile Levels final MediaCodecInfo.CodecProfileLevel[] codecProfileLevels = codecCapabilities.profileLevels; if (codecProfileLevels.length > 0) { final JSONArray profileLevels = new JSONArray(); for (MediaCodecInfo.CodecProfileLevel codecProfileLevel : codecProfileLevels) { final int profileValue = codecProfileLevel.profile; final int levelValue = codecProfileLevel.level; if (codecType == null) { // Unknown codec profileLevels.put(profileValue + '-' + levelValue); break; } switch (codecType) { case AVC: profileLevels.put(profileValue + mAVCProfileValues.get(profileValue) + '-' + mAVCLevelValues.get(levelValue)); break; case H263: profileLevels.put(mH263ProfileValues.get(profileValue) + '-' + mH263LevelValues.get(levelValue)); break; case MPEG4: profileLevels.put(mMPEG4ProfileValues.get(profileValue) + '-' + mMPEG4LevelValues.get(levelValue)); break; case AAC: profileLevels.put(mAACProfileValues.get(profileValue)); break; default: break; } } result.put("profileLevels", profileLevels); } return result; } /** * Looks for keywords in the codec name to identify its nature ({@link CodecType}). * * @param codecInfo the currently inspected codec * @return type of the codec or null if it could bot be guessed */ @Nullable @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private CodecType identifyCodecType(@NonNull MediaCodecInfo codecInfo) { final String name = codecInfo.getName(); for (String token : AVC_TYPES) { if (name.contains(token)) { return CodecType.AVC; } } for (String token : H263_TYPES) { if (name.contains(token)) { return CodecType.H263; } } for (String token : MPEG4_TYPES) { if (name.contains(token)) { return CodecType.MPEG4; } } for (String token : AAC_TYPES) { if (name.contains(token)) { return CodecType.AAC; } } return null; } }