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.hardware.DataSpace; 22 import android.media.MediaMetrics; 23 import android.media.codec.Enums; 24 import android.media.metrics.BundleSession; 25 import android.media.metrics.EditingEndedEvent; 26 import android.media.metrics.IMediaMetricsManager; 27 import android.media.metrics.MediaItemInfo; 28 import android.media.metrics.NetworkEvent; 29 import android.media.metrics.PlaybackErrorEvent; 30 import android.media.metrics.PlaybackMetrics; 31 import android.media.metrics.PlaybackStateEvent; 32 import android.media.metrics.TrackChangeEvent; 33 import android.os.Binder; 34 import android.os.PersistableBundle; 35 import android.provider.DeviceConfig; 36 import android.provider.DeviceConfig.Properties; 37 import android.text.TextUtils; 38 import android.util.Base64; 39 import android.util.Size; 40 import android.util.Slog; 41 import android.util.StatsEvent; 42 import android.util.StatsLog; 43 44 import com.android.internal.annotations.GuardedBy; 45 import com.android.server.SystemService; 46 47 import java.security.SecureRandom; 48 import java.util.Arrays; 49 import java.util.List; 50 import java.util.regex.Pattern; 51 52 /** 53 * System service manages media metrics. 54 */ 55 public final class MediaMetricsManagerService extends SystemService { 56 private static final String TAG = "MediaMetricsManagerService"; 57 58 private static final String MEDIA_METRICS_MODE = "media_metrics_mode"; 59 private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST = 60 "player_metrics_per_app_attribution_allowlist"; 61 private static final String PLAYER_METRICS_APP_ALLOWLIST = "player_metrics_app_allowlist"; 62 63 private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST = 64 "player_metrics_per_app_attribution_blocklist"; 65 private static final String PLAYER_METRICS_APP_BLOCKLIST = "player_metrics_app_blocklist"; 66 67 private static final int MEDIA_METRICS_MODE_OFF = 0; 68 private static final int MEDIA_METRICS_MODE_ON = 1; 69 private static final int MEDIA_METRICS_MODE_BLOCKLIST = 2; 70 private static final int MEDIA_METRICS_MODE_ALLOWLIST = 3; 71 72 // Cascading logging levels. The higher value, the more constrains (less logging data). 73 // The unused values between 2 consecutive levels are reserved for potential extra levels. 74 private static final int LOGGING_LEVEL_EVERYTHING = 0; 75 private static final int LOGGING_LEVEL_NO_UID = 1000; 76 private static final int LOGGING_LEVEL_BLOCKED = 99999; 77 78 private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER; 79 80 private static final String FAILED_TO_GET = "failed_to_get"; 81 82 private static final MediaItemInfo EMPTY_MEDIA_ITEM_INFO = new MediaItemInfo.Builder().build(); 83 private static final Pattern PATTERN_KNOWN_EDITING_LIBRARY_NAMES = 84 Pattern.compile( 85 "androidx\\.media3:media3-(transformer|muxer):" 86 + "[\\d.]+(-(alpha|beta|rc)\\d\\d)?"); 87 private static final int DURATION_BUCKETS_BELOW_ONE_MINUTE = 8; 88 private static final int DURATION_BUCKETS_COUNT = 13; 89 private static final String AUDIO_MIME_TYPE_PREFIX = "audio/"; 90 private static final String VIDEO_MIME_TYPE_PREFIX = "video/"; 91 private final SecureRandom mSecureRandom; 92 93 @GuardedBy("mLock") 94 private Integer mMode = null; 95 @GuardedBy("mLock") 96 private List<String> mAllowlist = null; 97 @GuardedBy("mLock") 98 private List<String> mNoUidAllowlist = null; 99 @GuardedBy("mLock") 100 private List<String> mBlockList = null; 101 @GuardedBy("mLock") 102 private List<String> mNoUidBlocklist = null; 103 private final Object mLock = new Object(); 104 private final Context mContext; 105 106 /** 107 * Initializes the playback metrics manager service. 108 * 109 * @param context The system server context. 110 */ MediaMetricsManagerService(Context context)111 public MediaMetricsManagerService(Context context) { 112 super(context); 113 mContext = context; 114 mSecureRandom = new SecureRandom(); 115 } 116 117 @Override onStart()118 public void onStart() { 119 publishBinderService(Context.MEDIA_METRICS_SERVICE, new BinderService()); 120 DeviceConfig.addOnPropertiesChangedListener( 121 DeviceConfig.NAMESPACE_MEDIA, 122 mContext.getMainExecutor(), 123 this::updateConfigs); 124 } 125 updateConfigs(Properties properties)126 private void updateConfigs(Properties properties) { 127 synchronized (mLock) { 128 mMode = properties.getInt( 129 MEDIA_METRICS_MODE, 130 MEDIA_METRICS_MODE_BLOCKLIST); 131 List<String> newList = getListLocked(PLAYER_METRICS_APP_ALLOWLIST); 132 if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) { 133 // don't overwrite the list if the mode IS MEDIA_METRICS_MODE_ALLOWLIST 134 // but failed to get 135 mAllowlist = newList; 136 } 137 newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST); 138 if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) { 139 mNoUidAllowlist = newList; 140 } 141 newList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST); 142 if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) { 143 mBlockList = newList; 144 } 145 newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST); 146 if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) { 147 mNoUidBlocklist = newList; 148 } 149 } 150 } 151 152 @GuardedBy("mLock") getListLocked(String listName)153 private List<String> getListLocked(String listName) { 154 final long identity = Binder.clearCallingIdentity(); 155 String listString = FAILED_TO_GET; 156 try { 157 listString = DeviceConfig.getString( 158 DeviceConfig.NAMESPACE_MEDIA, listName, FAILED_TO_GET); 159 } finally { 160 Binder.restoreCallingIdentity(identity); 161 } 162 if (listString.equals(FAILED_TO_GET)) { 163 Slog.d(TAG, "failed to get " + listName + " from DeviceConfig"); 164 return null; 165 } 166 String[] pkgArr = listString.split(","); 167 return Arrays.asList(pkgArr); 168 } 169 170 private final class BinderService extends IMediaMetricsManager.Stub { 171 @Override reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId)172 public void reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId) { 173 int level = loggingLevel(); 174 if (level == LOGGING_LEVEL_BLOCKED) { 175 return; 176 } 177 StatsEvent statsEvent = StatsEvent.newBuilder() 178 .setAtomId(320) 179 .writeInt(level == LOGGING_LEVEL_EVERYTHING ? Binder.getCallingUid() : 0) 180 .writeString(sessionId) 181 .writeLong(metrics.getMediaDurationMillis()) 182 .writeInt(metrics.getStreamSource()) 183 .writeInt(metrics.getStreamType()) 184 .writeInt(metrics.getPlaybackType()) 185 .writeInt(metrics.getDrmType()) 186 .writeInt(metrics.getContentType()) 187 .writeString(metrics.getPlayerName()) 188 .writeString(metrics.getPlayerVersion()) 189 .writeByteArray(new byte[0]) // TODO: write experiments proto 190 .writeInt(metrics.getVideoFramesPlayed()) 191 .writeInt(metrics.getVideoFramesDropped()) 192 .writeInt(metrics.getAudioUnderrunCount()) 193 .writeLong(metrics.getNetworkBytesRead()) 194 .writeLong(metrics.getLocalBytesRead()) 195 .writeLong(metrics.getNetworkTransferDurationMillis()) 196 // Raw bytes type not allowed in atoms 197 .writeString(Base64.encodeToString(metrics.getDrmSessionId(), Base64.DEFAULT)) 198 .usePooledBuffer() 199 .build(); 200 StatsLog.write(statsEvent); 201 } 202 reportBundleMetrics(String sessionId, PersistableBundle metrics, int userId)203 public void reportBundleMetrics(String sessionId, PersistableBundle metrics, int userId) { 204 int level = loggingLevel(); 205 if (level == LOGGING_LEVEL_BLOCKED) { 206 return; 207 } 208 209 int atomid = metrics.getInt(BundleSession.KEY_STATSD_ATOM); 210 switch (atomid) { 211 default: 212 return; 213 // to be extended as we define statsd atoms 214 case 322: // MediaPlaybackStateEvent 215 // pattern for the keys: 216 // <statsd event> - <fieldname> 217 // match types to what stats will want 218 String _sessionId = metrics.getString("playbackstateevent-sessionid"); 219 int _state = metrics.getInt("playbackstateevent-state", -1); 220 long _lifetime = metrics.getLong("playbackstateevent-lifetime", -1); 221 if (_sessionId == null || _state < 0 || _lifetime < 0) { 222 Slog.d(TAG, "dropping incomplete data for atom 322: _sessionId: " 223 + _sessionId + " _state: " + _state 224 + " _lifetime: " + _lifetime); 225 return; 226 } 227 StatsEvent statsEvent = StatsEvent.newBuilder() 228 .setAtomId(322) 229 .writeString(_sessionId) 230 .writeInt(_state) 231 .writeLong(_lifetime) 232 .usePooledBuffer() 233 .build(); 234 StatsLog.write(statsEvent); 235 return; 236 } 237 } 238 239 @Override reportPlaybackStateEvent( String sessionId, PlaybackStateEvent event, int userId)240 public void reportPlaybackStateEvent( 241 String sessionId, PlaybackStateEvent event, int userId) { 242 int level = loggingLevel(); 243 if (level == LOGGING_LEVEL_BLOCKED) { 244 return; 245 } 246 StatsEvent statsEvent = StatsEvent.newBuilder() 247 .setAtomId(322) 248 .writeString(sessionId) 249 .writeInt(event.getState()) 250 .writeLong(event.getTimeSinceCreatedMillis()) 251 .usePooledBuffer() 252 .build(); 253 StatsLog.write(statsEvent); 254 } 255 getSessionIdInternal(int userId)256 private String getSessionIdInternal(int userId) { 257 byte[] byteId = new byte[12]; // 96 bits (128 bits when expanded to Base64 string) 258 mSecureRandom.nextBytes(byteId); 259 String id = Base64.encodeToString( 260 byteId, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); 261 262 // Authorize these session ids in the native mediametrics service. 263 new MediaMetrics.Item(mMetricsId) 264 .set(MediaMetrics.Property.EVENT, "create") 265 .set(MediaMetrics.Property.LOG_SESSION_ID, id) 266 .record(); 267 return id; 268 } 269 270 @Override releaseSessionId(String sessionId, int userId)271 public void releaseSessionId(String sessionId, int userId) { 272 // De-authorize this session-id in the native mediametrics service. 273 // TODO: plumbing to the native mediametrics service 274 Slog.v(TAG, "Releasing sessionId " + sessionId + " for userId " + userId + " [NOP]"); 275 } 276 277 @Override getPlaybackSessionId(int userId)278 public String getPlaybackSessionId(int userId) { 279 return getSessionIdInternal(userId); 280 } 281 282 @Override getRecordingSessionId(int userId)283 public String getRecordingSessionId(int userId) { 284 return getSessionIdInternal(userId); 285 } 286 287 @Override getTranscodingSessionId(int userId)288 public String getTranscodingSessionId(int userId) { 289 return getSessionIdInternal(userId); 290 } 291 292 @Override getEditingSessionId(int userId)293 public String getEditingSessionId(int userId) { 294 return getSessionIdInternal(userId); 295 } 296 297 @Override getBundleSessionId(int userId)298 public String getBundleSessionId(int userId) { 299 return getSessionIdInternal(userId); 300 } 301 302 @Override reportPlaybackErrorEvent( String sessionId, PlaybackErrorEvent event, int userId)303 public void reportPlaybackErrorEvent( 304 String sessionId, PlaybackErrorEvent event, int userId) { 305 int level = loggingLevel(); 306 if (level == LOGGING_LEVEL_BLOCKED) { 307 return; 308 } 309 StatsEvent statsEvent = StatsEvent.newBuilder() 310 .setAtomId(323) 311 .writeString(sessionId) 312 .writeString(event.getExceptionStack()) 313 .writeInt(event.getErrorCode()) 314 .writeInt(event.getSubErrorCode()) 315 .writeLong(event.getTimeSinceCreatedMillis()) 316 .usePooledBuffer() 317 .build(); 318 StatsLog.write(statsEvent); 319 } 320 reportNetworkEvent( String sessionId, NetworkEvent event, int userId)321 public void reportNetworkEvent( 322 String sessionId, NetworkEvent event, int userId) { 323 int level = loggingLevel(); 324 if (level == LOGGING_LEVEL_BLOCKED) { 325 return; 326 } 327 StatsEvent statsEvent = StatsEvent.newBuilder() 328 .setAtomId(321) 329 .writeString(sessionId) 330 .writeInt(event.getNetworkType()) 331 .writeLong(event.getTimeSinceCreatedMillis()) 332 .usePooledBuffer() 333 .build(); 334 StatsLog.write(statsEvent); 335 } 336 337 @Override reportTrackChangeEvent( String sessionId, TrackChangeEvent event, int userId)338 public void reportTrackChangeEvent( 339 String sessionId, TrackChangeEvent event, int userId) { 340 int level = loggingLevel(); 341 if (level == LOGGING_LEVEL_BLOCKED) { 342 return; 343 } 344 StatsEvent statsEvent = StatsEvent.newBuilder() 345 .setAtomId(324) 346 .writeString(sessionId) 347 .writeInt(event.getTrackState()) 348 .writeInt(event.getTrackChangeReason()) 349 .writeString(event.getContainerMimeType()) 350 .writeString(event.getSampleMimeType()) 351 .writeString(event.getCodecName()) 352 .writeInt(event.getBitrate()) 353 .writeLong(event.getTimeSinceCreatedMillis()) 354 .writeInt(event.getTrackType()) 355 .writeString(event.getLanguage()) 356 .writeString(event.getLanguageRegion()) 357 .writeInt(event.getChannelCount()) 358 .writeInt(event.getAudioSampleRate()) 359 .writeInt(event.getWidth()) 360 .writeInt(event.getHeight()) 361 .writeFloat(event.getVideoFrameRate()) 362 .usePooledBuffer() 363 .build(); 364 StatsLog.write(statsEvent); 365 } 366 367 @Override reportEditingEndedEvent(String sessionId, EditingEndedEvent event, int userId)368 public void reportEditingEndedEvent(String sessionId, EditingEndedEvent event, int userId) { 369 int level = loggingLevel(); 370 if (level == LOGGING_LEVEL_BLOCKED) { 371 return; 372 } 373 MediaItemInfo inputMediaItemInfo = 374 event.getInputMediaItemInfos().isEmpty() 375 ? EMPTY_MEDIA_ITEM_INFO 376 : event.getInputMediaItemInfos().get(0); 377 @MediaItemInfo.DataType long inputDataTypes = inputMediaItemInfo.getDataTypes(); 378 String inputAudioSampleMimeType = 379 getFilteredFirstMimeType( 380 inputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX); 381 String inputVideoSampleMimeType = 382 getFilteredFirstMimeType( 383 inputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX); 384 Size inputVideoSize = inputMediaItemInfo.getVideoSize(); 385 int inputVideoResolution = getVideoResolutionEnum(inputVideoSize); 386 if (inputVideoResolution == Enums.RESOLUTION_UNKNOWN) { 387 // Try swapping width/height in case it's a portrait video. 388 inputVideoResolution = 389 getVideoResolutionEnum( 390 new Size(inputVideoSize.getHeight(), inputVideoSize.getWidth())); 391 } 392 List<String> inputCodecNames = inputMediaItemInfo.getCodecNames(); 393 String inputFirstCodecName = !inputCodecNames.isEmpty() ? inputCodecNames.get(0) : ""; 394 String inputSecondCodecName = inputCodecNames.size() > 1 ? inputCodecNames.get(1) : ""; 395 396 MediaItemInfo outputMediaItemInfo = 397 event.getOutputMediaItemInfo() == null 398 ? EMPTY_MEDIA_ITEM_INFO 399 : event.getOutputMediaItemInfo(); 400 @MediaItemInfo.DataType long outputDataTypes = outputMediaItemInfo.getDataTypes(); 401 String outputAudioSampleMimeType = 402 getFilteredFirstMimeType( 403 outputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX); 404 String outputVideoSampleMimeType = 405 getFilteredFirstMimeType( 406 outputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX); 407 Size outputVideoSize = outputMediaItemInfo.getVideoSize(); 408 int outputVideoResolution = getVideoResolutionEnum(outputVideoSize); 409 if (outputVideoResolution == Enums.RESOLUTION_UNKNOWN) { 410 // Try swapping width/height in case it's a portrait video. 411 outputVideoResolution = 412 getVideoResolutionEnum( 413 new Size(outputVideoSize.getHeight(), outputVideoSize.getWidth())); 414 } 415 List<String> outputCodecNames = outputMediaItemInfo.getCodecNames(); 416 String outputFirstCodecName = 417 !outputCodecNames.isEmpty() ? outputCodecNames.get(0) : ""; 418 String outputSecondCodecName = 419 outputCodecNames.size() > 1 ? outputCodecNames.get(1) : ""; 420 @EditingEndedEvent.OperationType long operationTypes = event.getOperationTypes(); 421 StatsEvent statsEvent = 422 StatsEvent.newBuilder() 423 .setAtomId(798) 424 .writeString(sessionId) 425 .writeInt(event.getFinalState()) 426 .writeFloat(event.getFinalProgressPercent()) 427 .writeInt(event.getErrorCode()) 428 .writeLong(event.getTimeSinceCreatedMillis()) 429 .writeBoolean( 430 (operationTypes 431 & EditingEndedEvent 432 .OPERATION_TYPE_VIDEO_TRANSCODE) 433 != 0) 434 .writeBoolean( 435 (operationTypes 436 & EditingEndedEvent 437 .OPERATION_TYPE_AUDIO_TRANSCODE) 438 != 0) 439 .writeBoolean( 440 (operationTypes & EditingEndedEvent.OPERATION_TYPE_VIDEO_EDIT) 441 != 0) 442 .writeBoolean( 443 (operationTypes & EditingEndedEvent.OPERATION_TYPE_AUDIO_EDIT) 444 != 0) 445 .writeBoolean( 446 (operationTypes 447 & EditingEndedEvent 448 .OPERATION_TYPE_VIDEO_TRANSMUX) 449 != 0) 450 .writeBoolean( 451 (operationTypes 452 & EditingEndedEvent 453 .OPERATION_TYPE_AUDIO_TRANSMUX) 454 != 0) 455 .writeBoolean( 456 (operationTypes & EditingEndedEvent.OPERATION_TYPE_PAUSED) != 0) 457 .writeBoolean( 458 (operationTypes & EditingEndedEvent.OPERATION_TYPE_RESUMED) 459 != 0) 460 .writeString(getFilteredLibraryName(event.getExporterName())) 461 .writeString(getFilteredLibraryName(event.getMuxerName())) 462 .writeInt(getThroughputFps(event)) 463 .writeInt(event.getInputMediaItemInfos().size()) 464 .writeInt(inputMediaItemInfo.getSourceType()) 465 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_IMAGE) != 0) 466 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_VIDEO) != 0) 467 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_AUDIO) != 0) 468 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_METADATA) != 0) 469 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_DEPTH) != 0) 470 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_GAIN_MAP) != 0) 471 .writeBoolean( 472 (inputDataTypes & MediaItemInfo.DATA_TYPE_HIGH_FRAME_RATE) != 0) 473 .writeBoolean( 474 (inputDataTypes 475 & MediaItemInfo 476 .DATA_TYPE_SPEED_SETTING_CUE_POINTS) 477 != 0) 478 .writeBoolean((inputDataTypes & MediaItemInfo.DATA_TYPE_GAPLESS) != 0) 479 .writeBoolean( 480 (inputDataTypes & MediaItemInfo.DATA_TYPE_SPATIAL_AUDIO) != 0) 481 .writeBoolean( 482 (inputDataTypes 483 & MediaItemInfo 484 .DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO) 485 != 0) 486 .writeLong( 487 getBucketedDurationMillis( 488 inputMediaItemInfo.getDurationMillis())) 489 .writeLong( 490 getBucketedDurationMillis( 491 inputMediaItemInfo.getClipDurationMillis())) 492 .writeString( 493 getFilteredMimeType(inputMediaItemInfo.getContainerMimeType())) 494 .writeString(inputAudioSampleMimeType) 495 .writeString(inputVideoSampleMimeType) 496 .writeInt(getCodecEnum(inputVideoSampleMimeType)) 497 .writeInt( 498 getFilteredAudioSampleRateHz( 499 inputMediaItemInfo.getAudioSampleRateHz())) 500 .writeInt(inputMediaItemInfo.getAudioChannelCount()) 501 .writeLong(inputMediaItemInfo.getAudioSampleCount()) 502 .writeInt(inputVideoSize.getWidth()) 503 .writeInt(inputVideoSize.getHeight()) 504 .writeInt(inputVideoResolution) 505 .writeInt(getVideoResolutionAspectRatioEnum(inputVideoSize)) 506 .writeInt(inputMediaItemInfo.getVideoDataSpace()) 507 .writeInt( 508 getVideoHdrFormatEnum( 509 inputMediaItemInfo.getVideoDataSpace(), 510 inputVideoSampleMimeType)) 511 .writeInt(Math.round(inputMediaItemInfo.getVideoFrameRate())) 512 .writeInt(getVideoFrameRateEnum(inputMediaItemInfo.getVideoFrameRate())) 513 .writeString(inputFirstCodecName) 514 .writeString(inputSecondCodecName) 515 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_IMAGE) != 0) 516 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_VIDEO) != 0) 517 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_AUDIO) != 0) 518 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_METADATA) != 0) 519 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_DEPTH) != 0) 520 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_GAIN_MAP) != 0) 521 .writeBoolean( 522 (outputDataTypes & MediaItemInfo.DATA_TYPE_HIGH_FRAME_RATE) 523 != 0) 524 .writeBoolean( 525 (outputDataTypes 526 & MediaItemInfo 527 .DATA_TYPE_SPEED_SETTING_CUE_POINTS) 528 != 0) 529 .writeBoolean((outputDataTypes & MediaItemInfo.DATA_TYPE_GAPLESS) != 0) 530 .writeBoolean( 531 (outputDataTypes & MediaItemInfo.DATA_TYPE_SPATIAL_AUDIO) != 0) 532 .writeBoolean( 533 (outputDataTypes 534 & MediaItemInfo 535 .DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO) 536 != 0) 537 .writeLong( 538 getBucketedDurationMillis( 539 outputMediaItemInfo.getDurationMillis())) 540 .writeLong( 541 getBucketedDurationMillis( 542 outputMediaItemInfo.getClipDurationMillis())) 543 .writeString( 544 getFilteredMimeType(outputMediaItemInfo.getContainerMimeType())) 545 .writeString(outputAudioSampleMimeType) 546 .writeString(outputVideoSampleMimeType) 547 .writeInt(getCodecEnum(outputVideoSampleMimeType)) 548 .writeInt( 549 getFilteredAudioSampleRateHz( 550 outputMediaItemInfo.getAudioSampleRateHz())) 551 .writeInt(outputMediaItemInfo.getAudioChannelCount()) 552 .writeLong(outputMediaItemInfo.getAudioSampleCount()) 553 .writeInt(outputVideoSize.getWidth()) 554 .writeInt(outputVideoSize.getHeight()) 555 .writeInt(outputVideoResolution) 556 .writeInt(getVideoResolutionAspectRatioEnum(outputVideoSize)) 557 .writeInt(outputMediaItemInfo.getVideoDataSpace()) 558 .writeInt( 559 getVideoHdrFormatEnum( 560 outputMediaItemInfo.getVideoDataSpace(), 561 outputVideoSampleMimeType)) 562 .writeInt(Math.round(outputMediaItemInfo.getVideoFrameRate())) 563 .writeInt( 564 getVideoFrameRateEnum(outputMediaItemInfo.getVideoFrameRate())) 565 .writeString(outputFirstCodecName) 566 .writeString(outputSecondCodecName) 567 .usePooledBuffer() 568 .build(); 569 StatsLog.write(statsEvent); 570 } 571 loggingLevel()572 private int loggingLevel() { 573 synchronized (mLock) { 574 int uid = Binder.getCallingUid(); 575 576 if (mMode == null) { 577 final long identity = Binder.clearCallingIdentity(); 578 try { 579 mMode = DeviceConfig.getInt( 580 DeviceConfig.NAMESPACE_MEDIA, 581 MEDIA_METRICS_MODE, 582 MEDIA_METRICS_MODE_BLOCKLIST); 583 } finally { 584 Binder.restoreCallingIdentity(identity); 585 } 586 } 587 588 if (mMode == MEDIA_METRICS_MODE_ON) { 589 return LOGGING_LEVEL_EVERYTHING; 590 } 591 if (mMode == MEDIA_METRICS_MODE_OFF) { 592 Slog.v(TAG, "Logging level blocked: MEDIA_METRICS_MODE_OFF"); 593 return LOGGING_LEVEL_BLOCKED; 594 } 595 596 PackageManager pm = getContext().getPackageManager(); 597 String[] packages = pm.getPackagesForUid(uid); 598 if (packages == null || packages.length == 0) { 599 // The valid application UID range is from 600 // android.os.Process.FIRST_APPLICATION_UID to 601 // android.os.Process.LAST_APPLICATION_UID. 602 // UIDs outside this range will not have a package. 603 Slog.d(TAG, "empty package from uid " + uid); 604 // block the data if the mode is MEDIA_METRICS_MODE_ALLOWLIST 605 return mMode == MEDIA_METRICS_MODE_BLOCKLIST 606 ? LOGGING_LEVEL_NO_UID : LOGGING_LEVEL_BLOCKED; 607 } 608 if (mMode == MEDIA_METRICS_MODE_BLOCKLIST) { 609 if (mBlockList == null) { 610 mBlockList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST); 611 if (mBlockList == null) { 612 // failed to get the blocklist. Block it. 613 Slog.v(TAG, "Logging level blocked: Failed to get " 614 + "PLAYER_METRICS_APP_BLOCKLIST."); 615 return LOGGING_LEVEL_BLOCKED; 616 } 617 } 618 Integer level = loggingLevelInternal( 619 packages, mBlockList, PLAYER_METRICS_APP_BLOCKLIST); 620 if (level != null) { 621 return level; 622 } 623 if (mNoUidBlocklist == null) { 624 mNoUidBlocklist = 625 getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST); 626 if (mNoUidBlocklist == null) { 627 // failed to get the blocklist. Block it. 628 Slog.v(TAG, "Logging level blocked: Failed to get " 629 + "PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST."); 630 return LOGGING_LEVEL_BLOCKED; 631 } 632 } 633 level = loggingLevelInternal( 634 packages, 635 mNoUidBlocklist, 636 PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST); 637 if (level != null) { 638 return level; 639 } 640 // Not detected in any blocklist. Log everything. 641 return LOGGING_LEVEL_EVERYTHING; 642 } 643 if (mMode == MEDIA_METRICS_MODE_ALLOWLIST) { 644 if (mNoUidAllowlist == null) { 645 mNoUidAllowlist = 646 getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST); 647 if (mNoUidAllowlist == null) { 648 // failed to get the allowlist. Block it. 649 Slog.v(TAG, "Logging level blocked: Failed to get " 650 + "PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST."); 651 return LOGGING_LEVEL_BLOCKED; 652 } 653 } 654 Integer level = loggingLevelInternal( 655 packages, 656 mNoUidAllowlist, 657 PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST); 658 if (level != null) { 659 return level; 660 } 661 if (mAllowlist == null) { 662 mAllowlist = getListLocked(PLAYER_METRICS_APP_ALLOWLIST); 663 if (mAllowlist == null) { 664 // failed to get the allowlist. Block it. 665 Slog.v(TAG, "Logging level blocked: Failed to get " 666 + "PLAYER_METRICS_APP_ALLOWLIST."); 667 return LOGGING_LEVEL_BLOCKED; 668 } 669 } 670 level = loggingLevelInternal( 671 packages, mAllowlist, PLAYER_METRICS_APP_ALLOWLIST); 672 if (level != null) { 673 return level; 674 } 675 // Not detected in any allowlist. Block. 676 Slog.v(TAG, "Logging level blocked: Not detected in any allowlist."); 677 return LOGGING_LEVEL_BLOCKED; 678 } 679 } 680 // Blocked by default. 681 Slog.v(TAG, "Logging level blocked: Blocked by default."); 682 return LOGGING_LEVEL_BLOCKED; 683 } 684 loggingLevelInternal( String[] packages, List<String> cached, String listName)685 private Integer loggingLevelInternal( 686 String[] packages, List<String> cached, String listName) { 687 if (inList(packages, cached)) { 688 return listNameToLoggingLevel(listName); 689 } 690 return null; 691 } 692 inList(String[] packages, List<String> arr)693 private boolean inList(String[] packages, List<String> arr) { 694 for (String p : packages) { 695 for (String element : arr) { 696 if (p.equals(element)) { 697 return true; 698 } 699 } 700 } 701 return false; 702 } 703 listNameToLoggingLevel(String listName)704 private int listNameToLoggingLevel(String listName) { 705 switch (listName) { 706 case PLAYER_METRICS_APP_BLOCKLIST: 707 return LOGGING_LEVEL_BLOCKED; 708 case PLAYER_METRICS_APP_ALLOWLIST: 709 return LOGGING_LEVEL_EVERYTHING; 710 case PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST: 711 case PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST: 712 return LOGGING_LEVEL_NO_UID; 713 default: 714 return LOGGING_LEVEL_BLOCKED; 715 } 716 } 717 } 718 getFilteredLibraryName(String libraryName)719 private static String getFilteredLibraryName(String libraryName) { 720 if (TextUtils.isEmpty(libraryName)) { 721 return ""; 722 } 723 if (!PATTERN_KNOWN_EDITING_LIBRARY_NAMES.matcher(libraryName).matches()) { 724 return ""; 725 } 726 return libraryName; 727 } 728 getThroughputFps(EditingEndedEvent event)729 private static int getThroughputFps(EditingEndedEvent event) { 730 MediaItemInfo outputMediaItemInfo = event.getOutputMediaItemInfo(); 731 if (outputMediaItemInfo == null) { 732 return -1; 733 } 734 long videoSampleCount = outputMediaItemInfo.getVideoSampleCount(); 735 if (videoSampleCount == MediaItemInfo.VALUE_UNSPECIFIED) { 736 return -1; 737 } 738 long elapsedTimeMs = event.getTimeSinceCreatedMillis(); 739 if (elapsedTimeMs == EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) { 740 return -1; 741 } 742 return (int) 743 Math.min(Integer.MAX_VALUE, Math.round(1000.0 * videoSampleCount / elapsedTimeMs)); 744 } 745 getBucketedDurationMillis(long durationMillis)746 private static long getBucketedDurationMillis(long durationMillis) { 747 if (durationMillis == MediaItemInfo.VALUE_UNSPECIFIED || durationMillis <= 0) { 748 return -1; 749 } 750 // Bucket values in an exponential distribution to reduce the precision that's stored: 751 // bucket index -> range -> bucketed duration 752 // 1 -> [0, 469 ms) -> 235 ms 753 // 2 -> [469 ms, 938 ms) -> 469 ms 754 // 3 -> [938 ms, 1875 ms) -> 938 ms 755 // 4 -> [1875 ms, 3750 ms) -> 1875 ms 756 // 5 -> [3750 ms, 7500 ms) -> 3750 ms 757 // [...] 758 // 13 -> [960000 ms, max) -> 960000 ms 759 int bucketIndex = 760 (int) 761 Math.floor( 762 DURATION_BUCKETS_BELOW_ONE_MINUTE 763 + Math.log((durationMillis + 1) / 60_000.0) / Math.log(2)); 764 // Clamp to range [0, DURATION_BUCKETS_COUNT]. 765 bucketIndex = Math.min(DURATION_BUCKETS_COUNT, Math.max(0, bucketIndex)); 766 // Map back onto the representative value for the bucket. 767 return (long) 768 Math.ceil(Math.pow(2, bucketIndex - DURATION_BUCKETS_BELOW_ONE_MINUTE) * 60_000.0); 769 } 770 771 /** 772 * Returns the first entry in {@code mimeTypes} with the given prefix, if it matches the 773 * filtering allowlist. If no entries match the prefix or if the first matching entry is not on 774 * the allowlist, returns an empty string. 775 */ getFilteredFirstMimeType(List<String> mimeTypes, String prefix)776 private static String getFilteredFirstMimeType(List<String> mimeTypes, String prefix) { 777 int size = mimeTypes.size(); 778 for (int i = 0; i < size; i++) { 779 String mimeType = mimeTypes.get(i); 780 if (mimeType.startsWith(prefix)) { 781 return getFilteredMimeType(mimeType); 782 } 783 } 784 return ""; 785 } 786 getFilteredMimeType(String mimeType)787 private static String getFilteredMimeType(String mimeType) { 788 if (TextUtils.isEmpty(mimeType)) { 789 return ""; 790 } 791 // Discard all inputs that aren't allowlisted MIME types. 792 return switch (mimeType) { 793 case "video/mp4", 794 "video/x-matroska", 795 "video/webm", 796 "video/3gpp", 797 "video/avc", 798 "video/hevc", 799 "video/x-vnd.on2.vp8", 800 "video/x-vnd.on2.vp9", 801 "video/av01", 802 "video/mp2t", 803 "video/mp4v-es", 804 "video/mpeg", 805 "video/x-flv", 806 "video/dolby-vision", 807 "video/raw", 808 "audio/mp4", 809 "audio/mp4a-latm", 810 "audio/x-matroska", 811 "audio/webm", 812 "audio/mpeg", 813 "audio/mpeg-L1", 814 "audio/mpeg-L2", 815 "audio/ac3", 816 "audio/eac3", 817 "audio/eac3-joc", 818 "audio/av4", 819 "audio/true-hd", 820 "audio/vnd.dts", 821 "audio/vnd.dts.hd", 822 "audio/vorbis", 823 "audio/opus", 824 "audio/flac", 825 "audio/ogg", 826 "audio/wav", 827 "audio/midi", 828 "audio/raw", 829 "application/mp4", 830 "application/webm", 831 "application/x-matroska", 832 "application/dash+xml", 833 "application/x-mpegURL", 834 "application/vnd.ms-sstr+xml" -> 835 mimeType; 836 default -> ""; 837 }; 838 } 839 getCodecEnum(String mimeType)840 private static int getCodecEnum(String mimeType) { 841 if (TextUtils.isEmpty(mimeType)) { 842 return Enums.CODEC_UNKNOWN; 843 } 844 return switch (mimeType) { 845 case "video/avc" -> Enums.CODEC_AVC; 846 case "video/hevc" -> Enums.CODEC_HEVC; 847 case "video/x-vnd.on2.vp8" -> Enums.CODEC_VP8; 848 case "video/x-vnd.on2.vp9" -> Enums.CODEC_VP9; 849 case "video/av01" -> Enums.CODEC_AV1; 850 default -> Enums.CODEC_UNKNOWN; 851 }; 852 } 853 854 private static int getFilteredAudioSampleRateHz(int sampleRateHz) { 855 return switch (sampleRateHz) { 856 case 8000, 11025, 16000, 22050, 44100, 48000, 96000, 192000 -> sampleRateHz; 857 default -> -1; 858 }; 859 } 860 861 private static int getVideoResolutionEnum(Size size) { 862 int width = size.getWidth(); 863 int height = size.getHeight(); 864 if (width == 352 && height == 640) { 865 return Enums.RESOLUTION_352X640; 866 } else if (width == 360 && height == 640) { 867 return Enums.RESOLUTION_360X640; 868 } else if (width == 480 && height == 640) { 869 return Enums.RESOLUTION_480X640; 870 } else if (width == 480 && height == 854) { 871 return Enums.RESOLUTION_480X854; 872 } else if (width == 540 && height == 960) { 873 return Enums.RESOLUTION_540X960; 874 } else if (width == 576 && height == 1024) { 875 return Enums.RESOLUTION_576X1024; 876 } else if (width == 1280 && height == 720) { 877 return Enums.RESOLUTION_720P_HD; 878 } else if (width == 1920 && height == 1080) { 879 return Enums.RESOLUTION_1080P_FHD; 880 } else if (width == 1440 && height == 2560) { 881 return Enums.RESOLUTION_1440X2560; 882 } else if (width == 3840 && height == 2160) { 883 return Enums.RESOLUTION_4K_UHD; 884 } else if (width == 7680 && height == 4320) { 885 return Enums.RESOLUTION_8K_UHD; 886 } else { 887 return Enums.RESOLUTION_UNKNOWN; 888 } 889 } 890 891 private static int getVideoResolutionAspectRatioEnum(Size size) { 892 int width = size.getWidth(); 893 int height = size.getHeight(); 894 if (width <= 0 || height <= 0) { 895 return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_UNSPECIFIED; 896 } else if (width < height) { 897 return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_PORTRAIT; 898 } else if (height < width) { 899 return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_LANDSCAPE; 900 } else { 901 return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_SQUARE; 902 } 903 } 904 905 private static int getVideoHdrFormatEnum(int dataSpace, String mimeType) { 906 if (dataSpace == DataSpace.DATASPACE_UNKNOWN) { 907 return Enums.HDR_FORMAT_UNKNOWN; 908 } 909 if (mimeType.equals("video/dolby-vision")) { 910 return Enums.HDR_FORMAT_DOLBY_VISION; 911 } 912 int standard = DataSpace.getStandard(dataSpace); 913 int transfer = DataSpace.getTransfer(dataSpace); 914 if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_HLG) { 915 return Enums.HDR_FORMAT_HLG; 916 } 917 if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_ST2084) { 918 // We don't currently distinguish HDR10+ from HDR10. 919 return Enums.HDR_FORMAT_HDR10; 920 } 921 return Enums.HDR_FORMAT_NONE; 922 } 923 924 private static int getVideoFrameRateEnum(float frameRate) { 925 int frameRateInt = Math.round(frameRate); 926 return switch (frameRateInt) { 927 case 24 -> Enums.FRAMERATE_24; 928 case 25 -> Enums.FRAMERATE_25; 929 case 30 -> Enums.FRAMERATE_30; 930 case 50 -> Enums.FRAMERATE_50; 931 case 60 -> Enums.FRAMERATE_60; 932 case 120 -> Enums.FRAMERATE_120; 933 case 240 -> Enums.FRAMERATE_240; 934 case 480 -> Enums.FRAMERATE_480; 935 case 960 -> Enums.FRAMERATE_960; 936 default -> Enums.FRAMERATE_UNKNOWN; 937 }; 938 } 939 } 940