• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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