blob: 2519bbf389bab8aeb9839a2e8e49943b9485c519 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* 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 com.android.server.media.metrics;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.metrics.IMediaMetricsManager;
import android.media.metrics.NetworkEvent;
import android.media.metrics.PlaybackErrorEvent;
import android.media.metrics.PlaybackMetrics;
import android.media.metrics.PlaybackStateEvent;
import android.media.metrics.TrackChangeEvent;
import android.os.Binder;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.Base64;
import android.util.Slog;
import android.util.StatsEvent;
import android.util.StatsLog;
import com.android.internal.annotations.GuardedBy;
import com.android.server.SystemService;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
/**
* System service manages media metrics.
*/
public final class MediaMetricsManagerService extends SystemService {
private static final String TAG = "MediaMetricsManagerService";
private static final String MEDIA_METRICS_MODE = "media_metrics_mode";
private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST =
"player_metrics_per_app_attribution_allowlist";
private static final String PLAYER_METRICS_APP_ALLOWLIST = "player_metrics_app_allowlist";
private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST =
"player_metrics_per_app_attribution_blocklist";
private static final String PLAYER_METRICS_APP_BLOCKLIST = "player_metrics_app_blocklist";
private static final int MEDIA_METRICS_MODE_OFF = 0;
private static final int MEDIA_METRICS_MODE_ON = 1;
private static final int MEDIA_METRICS_MODE_BLOCKLIST = 2;
private static final int MEDIA_METRICS_MODE_ALLOWLIST = 3;
// Cascading logging levels. The higher value, the more constrains (less logging data).
// The unused values between 2 consecutive levels are reserved for potential extra levels.
private static final int LOGGING_LEVEL_EVERYTHING = 0;
private static final int LOGGING_LEVEL_NO_UID = 1000;
private static final int LOGGING_LEVEL_BLOCKED = 99999;
private static final String FAILED_TO_GET = "failed_to_get";
private final SecureRandom mSecureRandom;
@GuardedBy("mLock")
private Integer mMode = null;
@GuardedBy("mLock")
private List<String> mAllowlist = null;
@GuardedBy("mLock")
private List<String> mNoUidAllowlist = null;
@GuardedBy("mLock")
private List<String> mBlockList = null;
@GuardedBy("mLock")
private List<String> mNoUidBlocklist = null;
private final Object mLock = new Object();
private final Context mContext;
/**
* Initializes the playback metrics manager service.
*
* @param context The system server context.
*/
public MediaMetricsManagerService(Context context) {
super(context);
mContext = context;
mSecureRandom = new SecureRandom();
}
@Override
public void onStart() {
publishBinderService(Context.MEDIA_METRICS_SERVICE, new BinderService());
DeviceConfig.addOnPropertiesChangedListener(
DeviceConfig.NAMESPACE_MEDIA,
mContext.getMainExecutor(),
this::updateConfigs);
}
private void updateConfigs(Properties properties) {
synchronized (mLock) {
mMode = properties.getInt(
MEDIA_METRICS_MODE,
MEDIA_METRICS_MODE_BLOCKLIST);
List<String> newList = getListLocked(PLAYER_METRICS_APP_ALLOWLIST);
if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) {
// don't overwrite the list if the mode IS MEDIA_METRICS_MODE_ALLOWLIST
// but failed to get
mAllowlist = newList;
}
newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST);
if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) {
mNoUidAllowlist = newList;
}
newList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST);
if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) {
mBlockList = newList;
}
newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST);
if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) {
mNoUidBlocklist = newList;
}
}
}
@GuardedBy("mLock")
private List<String> getListLocked(String listName) {
final long identity = Binder.clearCallingIdentity();
String listString = FAILED_TO_GET;
try {
listString = DeviceConfig.getString(
DeviceConfig.NAMESPACE_MEDIA, listName, FAILED_TO_GET);
} finally {
Binder.restoreCallingIdentity(identity);
}
if (listString.equals(FAILED_TO_GET)) {
Slog.d(TAG, "failed to get " + listName + " from DeviceConfig");
return null;
}
String[] pkgArr = listString.split(",");
return Arrays.asList(pkgArr);
}
private final class BinderService extends IMediaMetricsManager.Stub {
@Override
public void reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId) {
int level = loggingLevel();
if (level == LOGGING_LEVEL_BLOCKED) {
return;
}
StatsEvent statsEvent = StatsEvent.newBuilder()
.setAtomId(320)
.writeInt(level == LOGGING_LEVEL_EVERYTHING ? Binder.getCallingUid() : 0)
.writeString(sessionId)
.writeLong(metrics.getMediaDurationMillis())
.writeInt(metrics.getStreamSource())
.writeInt(metrics.getStreamType())
.writeInt(metrics.getPlaybackType())
.writeInt(metrics.getDrmType())
.writeInt(metrics.getContentType())
.writeString(metrics.getPlayerName())
.writeString(metrics.getPlayerVersion())
.writeByteArray(new byte[0]) // TODO: write experiments proto
.writeInt(metrics.getVideoFramesPlayed())
.writeInt(metrics.getVideoFramesDropped())
.writeInt(metrics.getAudioUnderrunCount())
.writeLong(metrics.getNetworkBytesRead())
.writeLong(metrics.getLocalBytesRead())
.writeLong(metrics.getNetworkTransferDurationMillis())
// Raw bytes type not allowed in atoms
.writeString(Base64.encodeToString(metrics.getDrmSessionId(), Base64.DEFAULT))
.usePooledBuffer()
.build();
StatsLog.write(statsEvent);
}
@Override
public void reportPlaybackStateEvent(
String sessionId, PlaybackStateEvent event, int userId) {
int level = loggingLevel();
if (level == LOGGING_LEVEL_BLOCKED) {
return;
}
StatsEvent statsEvent = StatsEvent.newBuilder()
.setAtomId(322)
.writeString(sessionId)
.writeInt(event.getState())
.writeLong(event.getTimeSinceCreatedMillis())
.usePooledBuffer()
.build();
StatsLog.write(statsEvent);
}
private String getSessionIdInternal(int userId) {
byte[] byteId = new byte[12]; // 96 bits (128 bits when expanded to Base64 string)
mSecureRandom.nextBytes(byteId);
String id = Base64.encodeToString(
byteId, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
return id;
}
@Override
public String getPlaybackSessionId(int userId) {
return getSessionIdInternal(userId);
}
@Override
public String getRecordingSessionId(int userId) {
return getSessionIdInternal(userId);
}
@Override
public void reportPlaybackErrorEvent(
String sessionId, PlaybackErrorEvent event, int userId) {
int level = loggingLevel();
if (level == LOGGING_LEVEL_BLOCKED) {
return;
}
StatsEvent statsEvent = StatsEvent.newBuilder()
.setAtomId(323)
.writeString(sessionId)
.writeString(event.getExceptionStack())
.writeInt(event.getErrorCode())
.writeInt(event.getSubErrorCode())
.writeLong(event.getTimeSinceCreatedMillis())
.usePooledBuffer()
.build();
StatsLog.write(statsEvent);
}
public void reportNetworkEvent(
String sessionId, NetworkEvent event, int userId) {
int level = loggingLevel();
if (level == LOGGING_LEVEL_BLOCKED) {
return;
}
StatsEvent statsEvent = StatsEvent.newBuilder()
.setAtomId(321)
.writeString(sessionId)
.writeInt(event.getNetworkType())
.writeLong(event.getTimeSinceCreatedMillis())
.usePooledBuffer()
.build();
StatsLog.write(statsEvent);
}
@Override
public void reportTrackChangeEvent(
String sessionId, TrackChangeEvent event, int userId) {
int level = loggingLevel();
if (level == LOGGING_LEVEL_BLOCKED) {
return;
}
StatsEvent statsEvent = StatsEvent.newBuilder()
.setAtomId(324)
.writeString(sessionId)
.writeInt(event.getTrackState())
.writeInt(event.getTrackChangeReason())
.writeString(event.getContainerMimeType())
.writeString(event.getSampleMimeType())
.writeString(event.getCodecName())
.writeInt(event.getBitrate())
.writeLong(event.getTimeSinceCreatedMillis())
.writeInt(event.getTrackType())
.writeString(event.getLanguage())
.writeString(event.getLanguageRegion())
.writeInt(event.getChannelCount())
.writeInt(event.getAudioSampleRate())
.writeInt(event.getWidth())
.writeInt(event.getHeight())
.writeFloat(event.getVideoFrameRate())
.usePooledBuffer()
.build();
StatsLog.write(statsEvent);
}
private int loggingLevel() {
synchronized (mLock) {
int uid = Binder.getCallingUid();
if (mMode == null) {
final long identity = Binder.clearCallingIdentity();
try {
mMode = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_MEDIA,
MEDIA_METRICS_MODE,
MEDIA_METRICS_MODE_BLOCKLIST);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
if (mMode == MEDIA_METRICS_MODE_ON) {
return LOGGING_LEVEL_EVERYTHING;
}
if (mMode == MEDIA_METRICS_MODE_OFF) {
return LOGGING_LEVEL_BLOCKED;
}
PackageManager pm = getContext().getPackageManager();
String[] packages = pm.getPackagesForUid(uid);
if (packages == null || packages.length == 0) {
// The valid application UID range is from
// android.os.Process.FIRST_APPLICATION_UID to
// android.os.Process.LAST_APPLICATION_UID.
// UIDs outside this range will not have a package.
Slog.d(TAG, "empty package from uid " + uid);
// block the data if the mode is MEDIA_METRICS_MODE_ALLOWLIST
return mMode == MEDIA_METRICS_MODE_BLOCKLIST
? LOGGING_LEVEL_NO_UID : LOGGING_LEVEL_BLOCKED;
}
if (mMode == MEDIA_METRICS_MODE_BLOCKLIST) {
if (mBlockList == null) {
mBlockList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST);
if (mBlockList == null) {
// failed to get the blocklist. Block it.
return LOGGING_LEVEL_BLOCKED;
}
}
Integer level = loggingLevelInternal(
packages, mBlockList, PLAYER_METRICS_APP_BLOCKLIST);
if (level != null) {
return level;
}
if (mNoUidBlocklist == null) {
mNoUidBlocklist =
getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST);
if (mNoUidBlocklist == null) {
// failed to get the blocklist. Block it.
return LOGGING_LEVEL_BLOCKED;
}
}
level = loggingLevelInternal(
packages,
mNoUidBlocklist,
PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST);
if (level != null) {
return level;
}
// Not detected in any blocklist. Log everything.
return LOGGING_LEVEL_EVERYTHING;
}
if (mMode == MEDIA_METRICS_MODE_ALLOWLIST) {
if (mNoUidAllowlist == null) {
mNoUidAllowlist =
getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST);
if (mNoUidAllowlist == null) {
// failed to get the allowlist. Block it.
return LOGGING_LEVEL_BLOCKED;
}
}
Integer level = loggingLevelInternal(
packages,
mNoUidAllowlist,
PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST);
if (level != null) {
return level;
}
if (mAllowlist == null) {
mAllowlist = getListLocked(PLAYER_METRICS_APP_ALLOWLIST);
if (mAllowlist == null) {
// failed to get the allowlist. Block it.
return LOGGING_LEVEL_BLOCKED;
}
}
level = loggingLevelInternal(
packages, mAllowlist, PLAYER_METRICS_APP_ALLOWLIST);
if (level != null) {
return level;
}
// Not detected in any allowlist. Block.
return LOGGING_LEVEL_BLOCKED;
}
}
// Blocked by default.
return LOGGING_LEVEL_BLOCKED;
}
private Integer loggingLevelInternal(
String[] packages, List<String> cached, String listName) {
if (inList(packages, cached)) {
return listNameToLoggingLevel(listName);
}
return null;
}
private boolean inList(String[] packages, List<String> arr) {
for (String p : packages) {
for (String element : arr) {
if (p.equals(element)) {
return true;
}
}
}
return false;
}
private int listNameToLoggingLevel(String listName) {
switch (listName) {
case PLAYER_METRICS_APP_BLOCKLIST:
return LOGGING_LEVEL_BLOCKED;
case PLAYER_METRICS_APP_ALLOWLIST:
return LOGGING_LEVEL_EVERYTHING;
case PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST:
case PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST:
return LOGGING_LEVEL_NO_UID;
default:
return LOGGING_LEVEL_BLOCKED;
}
}
}
}