1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.media.metrics; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.media.MediaMetrics; 22 import android.media.metrics.BundleSession; 23 import android.media.metrics.IMediaMetricsManager; 24 import android.media.metrics.NetworkEvent; 25 import android.media.metrics.PlaybackErrorEvent; 26 import android.media.metrics.PlaybackMetrics; 27 import android.media.metrics.PlaybackStateEvent; 28 import android.media.metrics.TrackChangeEvent; 29 import android.os.Binder; 30 import android.os.PersistableBundle; 31 import android.provider.DeviceConfig; 32 import android.provider.DeviceConfig.Properties; 33 import android.util.Base64; 34 import android.util.Slog; 35 import android.util.StatsEvent; 36 import android.util.StatsLog; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.server.SystemService; 40 41 import java.security.SecureRandom; 42 import java.util.Arrays; 43 import java.util.List; 44 45 /** 46 * System service manages media metrics. 47 */ 48 public final class MediaMetricsManagerService extends SystemService { 49 private static final String TAG = "MediaMetricsManagerService"; 50 51 private static final String MEDIA_METRICS_MODE = "media_metrics_mode"; 52 private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST = 53 "player_metrics_per_app_attribution_allowlist"; 54 private static final String PLAYER_METRICS_APP_ALLOWLIST = "player_metrics_app_allowlist"; 55 56 private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST = 57 "player_metrics_per_app_attribution_blocklist"; 58 private static final String PLAYER_METRICS_APP_BLOCKLIST = "player_metrics_app_blocklist"; 59 60 private static final int MEDIA_METRICS_MODE_OFF = 0; 61 private static final int MEDIA_METRICS_MODE_ON = 1; 62 private static final int MEDIA_METRICS_MODE_BLOCKLIST = 2; 63 private static final int MEDIA_METRICS_MODE_ALLOWLIST = 3; 64 65 // Cascading logging levels. The higher value, the more constrains (less logging data). 66 // The unused values between 2 consecutive levels are reserved for potential extra levels. 67 private static final int LOGGING_LEVEL_EVERYTHING = 0; 68 private static final int LOGGING_LEVEL_NO_UID = 1000; 69 private static final int LOGGING_LEVEL_BLOCKED = 99999; 70 71 private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER; 72 73 private static final String FAILED_TO_GET = "failed_to_get"; 74 private final SecureRandom mSecureRandom; 75 @GuardedBy("mLock") 76 private Integer mMode = null; 77 @GuardedBy("mLock") 78 private List<String> mAllowlist = null; 79 @GuardedBy("mLock") 80 private List<String> mNoUidAllowlist = null; 81 @GuardedBy("mLock") 82 private List<String> mBlockList = null; 83 @GuardedBy("mLock") 84 private List<String> mNoUidBlocklist = null; 85 private final Object mLock = new Object(); 86 private final Context mContext; 87 88 /** 89 * Initializes the playback metrics manager service. 90 * 91 * @param context The system server context. 92 */ MediaMetricsManagerService(Context context)93 public MediaMetricsManagerService(Context context) { 94 super(context); 95 mContext = context; 96 mSecureRandom = new SecureRandom(); 97 } 98 99 @Override onStart()100 public void onStart() { 101 publishBinderService(Context.MEDIA_METRICS_SERVICE, new BinderService()); 102 DeviceConfig.addOnPropertiesChangedListener( 103 DeviceConfig.NAMESPACE_MEDIA, 104 mContext.getMainExecutor(), 105 this::updateConfigs); 106 } 107 updateConfigs(Properties properties)108 private void updateConfigs(Properties properties) { 109 synchronized (mLock) { 110 mMode = properties.getInt( 111 MEDIA_METRICS_MODE, 112 MEDIA_METRICS_MODE_BLOCKLIST); 113 List<String> newList = getListLocked(PLAYER_METRICS_APP_ALLOWLIST); 114 if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) { 115 // don't overwrite the list if the mode IS MEDIA_METRICS_MODE_ALLOWLIST 116 // but failed to get 117 mAllowlist = newList; 118 } 119 newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST); 120 if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) { 121 mNoUidAllowlist = newList; 122 } 123 newList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST); 124 if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) { 125 mBlockList = newList; 126 } 127 newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST); 128 if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) { 129 mNoUidBlocklist = newList; 130 } 131 } 132 } 133 134 @GuardedBy("mLock") getListLocked(String listName)135 private List<String> getListLocked(String listName) { 136 final long identity = Binder.clearCallingIdentity(); 137 String listString = FAILED_TO_GET; 138 try { 139 listString = DeviceConfig.getString( 140 DeviceConfig.NAMESPACE_MEDIA, listName, FAILED_TO_GET); 141 } finally { 142 Binder.restoreCallingIdentity(identity); 143 } 144 if (listString.equals(FAILED_TO_GET)) { 145 Slog.d(TAG, "failed to get " + listName + " from DeviceConfig"); 146 return null; 147 } 148 String[] pkgArr = listString.split(","); 149 return Arrays.asList(pkgArr); 150 } 151 152 private final class BinderService extends IMediaMetricsManager.Stub { 153 @Override reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId)154 public void reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId) { 155 int level = loggingLevel(); 156 if (level == LOGGING_LEVEL_BLOCKED) { 157 return; 158 } 159 StatsEvent statsEvent = StatsEvent.newBuilder() 160 .setAtomId(320) 161 .writeInt(level == LOGGING_LEVEL_EVERYTHING ? Binder.getCallingUid() : 0) 162 .writeString(sessionId) 163 .writeLong(metrics.getMediaDurationMillis()) 164 .writeInt(metrics.getStreamSource()) 165 .writeInt(metrics.getStreamType()) 166 .writeInt(metrics.getPlaybackType()) 167 .writeInt(metrics.getDrmType()) 168 .writeInt(metrics.getContentType()) 169 .writeString(metrics.getPlayerName()) 170 .writeString(metrics.getPlayerVersion()) 171 .writeByteArray(new byte[0]) // TODO: write experiments proto 172 .writeInt(metrics.getVideoFramesPlayed()) 173 .writeInt(metrics.getVideoFramesDropped()) 174 .writeInt(metrics.getAudioUnderrunCount()) 175 .writeLong(metrics.getNetworkBytesRead()) 176 .writeLong(metrics.getLocalBytesRead()) 177 .writeLong(metrics.getNetworkTransferDurationMillis()) 178 // Raw bytes type not allowed in atoms 179 .writeString(Base64.encodeToString(metrics.getDrmSessionId(), Base64.DEFAULT)) 180 .usePooledBuffer() 181 .build(); 182 StatsLog.write(statsEvent); 183 } 184 reportBundleMetrics(String sessionId, PersistableBundle metrics, int userId)185 public void reportBundleMetrics(String sessionId, PersistableBundle metrics, int userId) { 186 int level = loggingLevel(); 187 if (level == LOGGING_LEVEL_BLOCKED) { 188 return; 189 } 190 191 int atomid = metrics.getInt(BundleSession.KEY_STATSD_ATOM); 192 switch (atomid) { 193 default: 194 return; 195 // to be extended as we define statsd atoms 196 case 322: // MediaPlaybackStateEvent 197 // pattern for the keys: 198 // <statsd event> - <fieldname> 199 // match types to what stats will want 200 String _sessionId = metrics.getString("playbackstateevent-sessionid"); 201 int _state = metrics.getInt("playbackstateevent-state", -1); 202 long _lifetime = metrics.getLong("playbackstateevent-lifetime", -1); 203 if (_sessionId == null || _state < 0 || _lifetime < 0) { 204 Slog.d(TAG, "dropping incomplete data for atom 322: _sessionId: " 205 + _sessionId + " _state: " + _state 206 + " _lifetime: " + _lifetime); 207 return; 208 } 209 StatsEvent statsEvent = StatsEvent.newBuilder() 210 .setAtomId(322) 211 .writeString(_sessionId) 212 .writeInt(_state) 213 .writeLong(_lifetime) 214 .usePooledBuffer() 215 .build(); 216 StatsLog.write(statsEvent); 217 return; 218 } 219 } 220 221 @Override reportPlaybackStateEvent( String sessionId, PlaybackStateEvent event, int userId)222 public void reportPlaybackStateEvent( 223 String sessionId, PlaybackStateEvent event, int userId) { 224 int level = loggingLevel(); 225 if (level == LOGGING_LEVEL_BLOCKED) { 226 return; 227 } 228 StatsEvent statsEvent = StatsEvent.newBuilder() 229 .setAtomId(322) 230 .writeString(sessionId) 231 .writeInt(event.getState()) 232 .writeLong(event.getTimeSinceCreatedMillis()) 233 .usePooledBuffer() 234 .build(); 235 StatsLog.write(statsEvent); 236 } 237 getSessionIdInternal(int userId)238 private String getSessionIdInternal(int userId) { 239 byte[] byteId = new byte[12]; // 96 bits (128 bits when expanded to Base64 string) 240 mSecureRandom.nextBytes(byteId); 241 String id = Base64.encodeToString( 242 byteId, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); 243 244 // Authorize these session ids in the native mediametrics service. 245 new MediaMetrics.Item(mMetricsId) 246 .set(MediaMetrics.Property.EVENT, "create") 247 .set(MediaMetrics.Property.LOG_SESSION_ID, id) 248 .record(); 249 return id; 250 } 251 252 @Override releaseSessionId(String sessionId, int userId)253 public void releaseSessionId(String sessionId, int userId) { 254 // De-authorize this session-id in the native mediametrics service. 255 // TODO: plumbing to the native mediametrics service 256 Slog.v(TAG, "Releasing sessionId " + sessionId + " for userId " + userId + " [NOP]"); 257 } 258 259 @Override getPlaybackSessionId(int userId)260 public String getPlaybackSessionId(int userId) { 261 return getSessionIdInternal(userId); 262 } 263 264 @Override getRecordingSessionId(int userId)265 public String getRecordingSessionId(int userId) { 266 return getSessionIdInternal(userId); 267 } 268 269 @Override getTranscodingSessionId(int userId)270 public String getTranscodingSessionId(int userId) { 271 return getSessionIdInternal(userId); 272 } 273 274 @Override getEditingSessionId(int userId)275 public String getEditingSessionId(int userId) { 276 return getSessionIdInternal(userId); 277 } 278 279 @Override getBundleSessionId(int userId)280 public String getBundleSessionId(int userId) { 281 return getSessionIdInternal(userId); 282 } 283 284 @Override reportPlaybackErrorEvent( String sessionId, PlaybackErrorEvent event, int userId)285 public void reportPlaybackErrorEvent( 286 String sessionId, PlaybackErrorEvent event, int userId) { 287 int level = loggingLevel(); 288 if (level == LOGGING_LEVEL_BLOCKED) { 289 return; 290 } 291 StatsEvent statsEvent = StatsEvent.newBuilder() 292 .setAtomId(323) 293 .writeString(sessionId) 294 .writeString(event.getExceptionStack()) 295 .writeInt(event.getErrorCode()) 296 .writeInt(event.getSubErrorCode()) 297 .writeLong(event.getTimeSinceCreatedMillis()) 298 .usePooledBuffer() 299 .build(); 300 StatsLog.write(statsEvent); 301 } 302 reportNetworkEvent( String sessionId, NetworkEvent event, int userId)303 public void reportNetworkEvent( 304 String sessionId, NetworkEvent event, int userId) { 305 int level = loggingLevel(); 306 if (level == LOGGING_LEVEL_BLOCKED) { 307 return; 308 } 309 StatsEvent statsEvent = StatsEvent.newBuilder() 310 .setAtomId(321) 311 .writeString(sessionId) 312 .writeInt(event.getNetworkType()) 313 .writeLong(event.getTimeSinceCreatedMillis()) 314 .usePooledBuffer() 315 .build(); 316 StatsLog.write(statsEvent); 317 } 318 319 @Override reportTrackChangeEvent( String sessionId, TrackChangeEvent event, int userId)320 public void reportTrackChangeEvent( 321 String sessionId, TrackChangeEvent event, int userId) { 322 int level = loggingLevel(); 323 if (level == LOGGING_LEVEL_BLOCKED) { 324 return; 325 } 326 StatsEvent statsEvent = StatsEvent.newBuilder() 327 .setAtomId(324) 328 .writeString(sessionId) 329 .writeInt(event.getTrackState()) 330 .writeInt(event.getTrackChangeReason()) 331 .writeString(event.getContainerMimeType()) 332 .writeString(event.getSampleMimeType()) 333 .writeString(event.getCodecName()) 334 .writeInt(event.getBitrate()) 335 .writeLong(event.getTimeSinceCreatedMillis()) 336 .writeInt(event.getTrackType()) 337 .writeString(event.getLanguage()) 338 .writeString(event.getLanguageRegion()) 339 .writeInt(event.getChannelCount()) 340 .writeInt(event.getAudioSampleRate()) 341 .writeInt(event.getWidth()) 342 .writeInt(event.getHeight()) 343 .writeFloat(event.getVideoFrameRate()) 344 .usePooledBuffer() 345 .build(); 346 StatsLog.write(statsEvent); 347 } 348 loggingLevel()349 private int loggingLevel() { 350 synchronized (mLock) { 351 int uid = Binder.getCallingUid(); 352 353 if (mMode == null) { 354 final long identity = Binder.clearCallingIdentity(); 355 try { 356 mMode = DeviceConfig.getInt( 357 DeviceConfig.NAMESPACE_MEDIA, 358 MEDIA_METRICS_MODE, 359 MEDIA_METRICS_MODE_BLOCKLIST); 360 } finally { 361 Binder.restoreCallingIdentity(identity); 362 } 363 } 364 365 if (mMode == MEDIA_METRICS_MODE_ON) { 366 return LOGGING_LEVEL_EVERYTHING; 367 } 368 if (mMode == MEDIA_METRICS_MODE_OFF) { 369 Slog.v(TAG, "Logging level blocked: MEDIA_METRICS_MODE_OFF"); 370 return LOGGING_LEVEL_BLOCKED; 371 } 372 373 PackageManager pm = getContext().getPackageManager(); 374 String[] packages = pm.getPackagesForUid(uid); 375 if (packages == null || packages.length == 0) { 376 // The valid application UID range is from 377 // android.os.Process.FIRST_APPLICATION_UID to 378 // android.os.Process.LAST_APPLICATION_UID. 379 // UIDs outside this range will not have a package. 380 Slog.d(TAG, "empty package from uid " + uid); 381 // block the data if the mode is MEDIA_METRICS_MODE_ALLOWLIST 382 return mMode == MEDIA_METRICS_MODE_BLOCKLIST 383 ? LOGGING_LEVEL_NO_UID : LOGGING_LEVEL_BLOCKED; 384 } 385 if (mMode == MEDIA_METRICS_MODE_BLOCKLIST) { 386 if (mBlockList == null) { 387 mBlockList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST); 388 if (mBlockList == null) { 389 // failed to get the blocklist. Block it. 390 Slog.v(TAG, "Logging level blocked: Failed to get " 391 + "PLAYER_METRICS_APP_BLOCKLIST."); 392 return LOGGING_LEVEL_BLOCKED; 393 } 394 } 395 Integer level = loggingLevelInternal( 396 packages, mBlockList, PLAYER_METRICS_APP_BLOCKLIST); 397 if (level != null) { 398 return level; 399 } 400 if (mNoUidBlocklist == null) { 401 mNoUidBlocklist = 402 getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST); 403 if (mNoUidBlocklist == null) { 404 // failed to get the blocklist. Block it. 405 Slog.v(TAG, "Logging level blocked: Failed to get " 406 + "PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST."); 407 return LOGGING_LEVEL_BLOCKED; 408 } 409 } 410 level = loggingLevelInternal( 411 packages, 412 mNoUidBlocklist, 413 PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST); 414 if (level != null) { 415 return level; 416 } 417 // Not detected in any blocklist. Log everything. 418 return LOGGING_LEVEL_EVERYTHING; 419 } 420 if (mMode == MEDIA_METRICS_MODE_ALLOWLIST) { 421 if (mNoUidAllowlist == null) { 422 mNoUidAllowlist = 423 getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST); 424 if (mNoUidAllowlist == null) { 425 // failed to get the allowlist. Block it. 426 Slog.v(TAG, "Logging level blocked: Failed to get " 427 + "PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST."); 428 return LOGGING_LEVEL_BLOCKED; 429 } 430 } 431 Integer level = loggingLevelInternal( 432 packages, 433 mNoUidAllowlist, 434 PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST); 435 if (level != null) { 436 return level; 437 } 438 if (mAllowlist == null) { 439 mAllowlist = getListLocked(PLAYER_METRICS_APP_ALLOWLIST); 440 if (mAllowlist == null) { 441 // failed to get the allowlist. Block it. 442 Slog.v(TAG, "Logging level blocked: Failed to get " 443 + "PLAYER_METRICS_APP_ALLOWLIST."); 444 return LOGGING_LEVEL_BLOCKED; 445 } 446 } 447 level = loggingLevelInternal( 448 packages, mAllowlist, PLAYER_METRICS_APP_ALLOWLIST); 449 if (level != null) { 450 return level; 451 } 452 // Not detected in any allowlist. Block. 453 Slog.v(TAG, "Logging level blocked: Not detected in any allowlist."); 454 return LOGGING_LEVEL_BLOCKED; 455 } 456 } 457 // Blocked by default. 458 Slog.v(TAG, "Logging level blocked: Blocked by default."); 459 return LOGGING_LEVEL_BLOCKED; 460 } 461 loggingLevelInternal( String[] packages, List<String> cached, String listName)462 private Integer loggingLevelInternal( 463 String[] packages, List<String> cached, String listName) { 464 if (inList(packages, cached)) { 465 return listNameToLoggingLevel(listName); 466 } 467 return null; 468 } 469 inList(String[] packages, List<String> arr)470 private boolean inList(String[] packages, List<String> arr) { 471 for (String p : packages) { 472 for (String element : arr) { 473 if (p.equals(element)) { 474 return true; 475 } 476 } 477 } 478 return false; 479 } 480 listNameToLoggingLevel(String listName)481 private int listNameToLoggingLevel(String listName) { 482 switch (listName) { 483 case PLAYER_METRICS_APP_BLOCKLIST: 484 return LOGGING_LEVEL_BLOCKED; 485 case PLAYER_METRICS_APP_ALLOWLIST: 486 return LOGGING_LEVEL_EVERYTHING; 487 case PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST: 488 case PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST: 489 return LOGGING_LEVEL_NO_UID; 490 default: 491 return LOGGING_LEVEL_BLOCKED; 492 } 493 } 494 } 495 } 496