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