/*
* 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;
}
}