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.IMediaMetricsManager;
23 import android.media.metrics.NetworkEvent;
24 import android.media.metrics.PlaybackErrorEvent;
25 import android.media.metrics.PlaybackMetrics;
26 import android.media.metrics.PlaybackStateEvent;
27 import android.media.metrics.TrackChangeEvent;
28 import android.os.Binder;
29 import android.provider.DeviceConfig;
30 import android.provider.DeviceConfig.Properties;
31 import android.util.Base64;
32 import android.util.Slog;
33 import android.util.StatsEvent;
34 import android.util.StatsLog;
35 
36 import com.android.internal.annotations.GuardedBy;
37 import com.android.server.SystemService;
38 
39 import java.security.SecureRandom;
40 import java.util.Arrays;
41 import java.util.List;
42 
43 /**
44  * System service manages media metrics.
45  */
46 public final class MediaMetricsManagerService extends SystemService {
47     private static final String TAG = "MediaMetricsManagerService";
48 
49     private static final String MEDIA_METRICS_MODE = "media_metrics_mode";
50     private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST =
51             "player_metrics_per_app_attribution_allowlist";
52     private static final String PLAYER_METRICS_APP_ALLOWLIST = "player_metrics_app_allowlist";
53 
54     private static final String PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST =
55             "player_metrics_per_app_attribution_blocklist";
56     private static final String PLAYER_METRICS_APP_BLOCKLIST = "player_metrics_app_blocklist";
57 
58     private static final int MEDIA_METRICS_MODE_OFF = 0;
59     private static final int MEDIA_METRICS_MODE_ON = 1;
60     private static final int MEDIA_METRICS_MODE_BLOCKLIST = 2;
61     private static final int MEDIA_METRICS_MODE_ALLOWLIST = 3;
62 
63     // Cascading logging levels. The higher value, the more constrains (less logging data).
64     // The unused values between 2 consecutive levels are reserved for potential extra levels.
65     private static final int LOGGING_LEVEL_EVERYTHING = 0;
66     private static final int LOGGING_LEVEL_NO_UID = 1000;
67     private static final int LOGGING_LEVEL_BLOCKED = 99999;
68 
69     private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER;
70 
71     private static final String FAILED_TO_GET = "failed_to_get";
72     private final SecureRandom mSecureRandom;
73     @GuardedBy("mLock")
74     private Integer mMode = null;
75     @GuardedBy("mLock")
76     private List<String> mAllowlist = null;
77     @GuardedBy("mLock")
78     private List<String> mNoUidAllowlist = null;
79     @GuardedBy("mLock")
80     private List<String> mBlockList = null;
81     @GuardedBy("mLock")
82     private List<String> mNoUidBlocklist = null;
83     private final Object mLock = new Object();
84     private final Context mContext;
85 
86     /**
87      * Initializes the playback metrics manager service.
88      *
89      * @param context The system server context.
90      */
MediaMetricsManagerService(Context context)91     public MediaMetricsManagerService(Context context) {
92         super(context);
93         mContext = context;
94         mSecureRandom = new SecureRandom();
95     }
96 
97     @Override
onStart()98     public void onStart() {
99         publishBinderService(Context.MEDIA_METRICS_SERVICE, new BinderService());
100         DeviceConfig.addOnPropertiesChangedListener(
101                 DeviceConfig.NAMESPACE_MEDIA,
102                 mContext.getMainExecutor(),
103                 this::updateConfigs);
104     }
105 
updateConfigs(Properties properties)106     private void updateConfigs(Properties properties) {
107         synchronized (mLock) {
108             mMode = properties.getInt(
109                     MEDIA_METRICS_MODE,
110                     MEDIA_METRICS_MODE_BLOCKLIST);
111             List<String> newList = getListLocked(PLAYER_METRICS_APP_ALLOWLIST);
112             if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) {
113                 // don't overwrite the list if the mode IS MEDIA_METRICS_MODE_ALLOWLIST
114                 // but failed to get
115                 mAllowlist = newList;
116             }
117             newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST);
118             if (newList != null || mMode != MEDIA_METRICS_MODE_ALLOWLIST) {
119                 mNoUidAllowlist = newList;
120             }
121             newList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST);
122             if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) {
123                 mBlockList = newList;
124             }
125             newList = getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST);
126             if (newList != null || mMode != MEDIA_METRICS_MODE_BLOCKLIST) {
127                 mNoUidBlocklist = newList;
128             }
129         }
130     }
131 
132     @GuardedBy("mLock")
getListLocked(String listName)133     private List<String> getListLocked(String listName) {
134         final long identity = Binder.clearCallingIdentity();
135         String listString = FAILED_TO_GET;
136         try {
137             listString = DeviceConfig.getString(
138                     DeviceConfig.NAMESPACE_MEDIA, listName, FAILED_TO_GET);
139         } finally {
140             Binder.restoreCallingIdentity(identity);
141         }
142         if (listString.equals(FAILED_TO_GET)) {
143             Slog.d(TAG, "failed to get " + listName + " from DeviceConfig");
144             return null;
145         }
146         String[] pkgArr = listString.split(",");
147         return Arrays.asList(pkgArr);
148     }
149 
150     private final class BinderService extends IMediaMetricsManager.Stub {
151         @Override
reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId)152         public void reportPlaybackMetrics(String sessionId, PlaybackMetrics metrics, int userId) {
153             int level = loggingLevel();
154             if (level == LOGGING_LEVEL_BLOCKED) {
155                 return;
156             }
157             StatsEvent statsEvent = StatsEvent.newBuilder()
158                     .setAtomId(320)
159                     .writeInt(level == LOGGING_LEVEL_EVERYTHING ? Binder.getCallingUid() : 0)
160                     .writeString(sessionId)
161                     .writeLong(metrics.getMediaDurationMillis())
162                     .writeInt(metrics.getStreamSource())
163                     .writeInt(metrics.getStreamType())
164                     .writeInt(metrics.getPlaybackType())
165                     .writeInt(metrics.getDrmType())
166                     .writeInt(metrics.getContentType())
167                     .writeString(metrics.getPlayerName())
168                     .writeString(metrics.getPlayerVersion())
169                     .writeByteArray(new byte[0]) // TODO: write experiments proto
170                     .writeInt(metrics.getVideoFramesPlayed())
171                     .writeInt(metrics.getVideoFramesDropped())
172                     .writeInt(metrics.getAudioUnderrunCount())
173                     .writeLong(metrics.getNetworkBytesRead())
174                     .writeLong(metrics.getLocalBytesRead())
175                     .writeLong(metrics.getNetworkTransferDurationMillis())
176                     // Raw bytes type not allowed in atoms
177                     .writeString(Base64.encodeToString(metrics.getDrmSessionId(), Base64.DEFAULT))
178                     .usePooledBuffer()
179                     .build();
180             StatsLog.write(statsEvent);
181         }
182 
183         @Override
reportPlaybackStateEvent( String sessionId, PlaybackStateEvent event, int userId)184         public void reportPlaybackStateEvent(
185                 String sessionId, PlaybackStateEvent event, int userId) {
186             int level = loggingLevel();
187             if (level == LOGGING_LEVEL_BLOCKED) {
188                 return;
189             }
190             StatsEvent statsEvent = StatsEvent.newBuilder()
191                     .setAtomId(322)
192                     .writeString(sessionId)
193                     .writeInt(event.getState())
194                     .writeLong(event.getTimeSinceCreatedMillis())
195                     .usePooledBuffer()
196                     .build();
197             StatsLog.write(statsEvent);
198         }
199 
getSessionIdInternal(int userId)200         private String getSessionIdInternal(int userId) {
201             byte[] byteId = new byte[12]; // 96 bits (128 bits when expanded to Base64 string)
202             mSecureRandom.nextBytes(byteId);
203             String id = Base64.encodeToString(
204                     byteId, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
205 
206             // Authorize these session ids in the native mediametrics service.
207             new MediaMetrics.Item(mMetricsId)
208                     .set(MediaMetrics.Property.EVENT, "create")
209                     .set(MediaMetrics.Property.LOG_SESSION_ID, id)
210                     .record();
211             return id;
212         }
213 
214         @Override
getPlaybackSessionId(int userId)215         public String getPlaybackSessionId(int userId) {
216             return getSessionIdInternal(userId);
217         }
218 
219         @Override
getRecordingSessionId(int userId)220         public String getRecordingSessionId(int userId) {
221             return getSessionIdInternal(userId);
222         }
223 
224         @Override
reportPlaybackErrorEvent( String sessionId, PlaybackErrorEvent event, int userId)225         public void reportPlaybackErrorEvent(
226                 String sessionId, PlaybackErrorEvent event, int userId) {
227             int level = loggingLevel();
228             if (level == LOGGING_LEVEL_BLOCKED) {
229                 return;
230             }
231             StatsEvent statsEvent = StatsEvent.newBuilder()
232                     .setAtomId(323)
233                     .writeString(sessionId)
234                     .writeString(event.getExceptionStack())
235                     .writeInt(event.getErrorCode())
236                     .writeInt(event.getSubErrorCode())
237                     .writeLong(event.getTimeSinceCreatedMillis())
238                     .usePooledBuffer()
239                     .build();
240             StatsLog.write(statsEvent);
241         }
242 
reportNetworkEvent( String sessionId, NetworkEvent event, int userId)243         public void reportNetworkEvent(
244                 String sessionId, NetworkEvent event, int userId) {
245             int level = loggingLevel();
246             if (level == LOGGING_LEVEL_BLOCKED) {
247                 return;
248             }
249             StatsEvent statsEvent = StatsEvent.newBuilder()
250                     .setAtomId(321)
251                     .writeString(sessionId)
252                     .writeInt(event.getNetworkType())
253                     .writeLong(event.getTimeSinceCreatedMillis())
254                     .usePooledBuffer()
255                     .build();
256             StatsLog.write(statsEvent);
257         }
258 
259         @Override
reportTrackChangeEvent( String sessionId, TrackChangeEvent event, int userId)260         public void reportTrackChangeEvent(
261                 String sessionId, TrackChangeEvent event, int userId) {
262             int level = loggingLevel();
263             if (level == LOGGING_LEVEL_BLOCKED) {
264                 return;
265             }
266             StatsEvent statsEvent = StatsEvent.newBuilder()
267                     .setAtomId(324)
268                     .writeString(sessionId)
269                     .writeInt(event.getTrackState())
270                     .writeInt(event.getTrackChangeReason())
271                     .writeString(event.getContainerMimeType())
272                     .writeString(event.getSampleMimeType())
273                     .writeString(event.getCodecName())
274                     .writeInt(event.getBitrate())
275                     .writeLong(event.getTimeSinceCreatedMillis())
276                     .writeInt(event.getTrackType())
277                     .writeString(event.getLanguage())
278                     .writeString(event.getLanguageRegion())
279                     .writeInt(event.getChannelCount())
280                     .writeInt(event.getAudioSampleRate())
281                     .writeInt(event.getWidth())
282                     .writeInt(event.getHeight())
283                     .writeFloat(event.getVideoFrameRate())
284                     .usePooledBuffer()
285                     .build();
286             StatsLog.write(statsEvent);
287         }
288 
loggingLevel()289         private int loggingLevel() {
290             synchronized (mLock) {
291                 int uid = Binder.getCallingUid();
292 
293                 if (mMode == null) {
294                     final long identity = Binder.clearCallingIdentity();
295                     try {
296                         mMode = DeviceConfig.getInt(
297                             DeviceConfig.NAMESPACE_MEDIA,
298                             MEDIA_METRICS_MODE,
299                             MEDIA_METRICS_MODE_BLOCKLIST);
300                     } finally {
301                         Binder.restoreCallingIdentity(identity);
302                     }
303                 }
304 
305                 if (mMode == MEDIA_METRICS_MODE_ON) {
306                     return LOGGING_LEVEL_EVERYTHING;
307                 }
308                 if (mMode == MEDIA_METRICS_MODE_OFF) {
309                     return LOGGING_LEVEL_BLOCKED;
310                 }
311 
312                 PackageManager pm = getContext().getPackageManager();
313                 String[] packages = pm.getPackagesForUid(uid);
314                 if (packages == null || packages.length == 0) {
315                     // The valid application UID range is from
316                     // android.os.Process.FIRST_APPLICATION_UID to
317                     // android.os.Process.LAST_APPLICATION_UID.
318                     // UIDs outside this range will not have a package.
319                     Slog.d(TAG, "empty package from uid " + uid);
320                     // block the data if the mode is MEDIA_METRICS_MODE_ALLOWLIST
321                     return mMode == MEDIA_METRICS_MODE_BLOCKLIST
322                             ? LOGGING_LEVEL_NO_UID : LOGGING_LEVEL_BLOCKED;
323                 }
324                 if (mMode == MEDIA_METRICS_MODE_BLOCKLIST) {
325                     if (mBlockList == null) {
326                         mBlockList = getListLocked(PLAYER_METRICS_APP_BLOCKLIST);
327                         if (mBlockList == null) {
328                             // failed to get the blocklist. Block it.
329                             return LOGGING_LEVEL_BLOCKED;
330                         }
331                     }
332                     Integer level = loggingLevelInternal(
333                             packages, mBlockList, PLAYER_METRICS_APP_BLOCKLIST);
334                     if (level != null) {
335                         return level;
336                     }
337                     if (mNoUidBlocklist == null) {
338                         mNoUidBlocklist =
339                                 getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST);
340                         if (mNoUidBlocklist == null) {
341                             // failed to get the blocklist. Block it.
342                             return LOGGING_LEVEL_BLOCKED;
343                         }
344                     }
345                     level = loggingLevelInternal(
346                             packages,
347                             mNoUidBlocklist,
348                             PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST);
349                     if (level != null) {
350                         return level;
351                     }
352                     // Not detected in any blocklist. Log everything.
353                     return LOGGING_LEVEL_EVERYTHING;
354                 }
355                 if (mMode == MEDIA_METRICS_MODE_ALLOWLIST) {
356                     if (mNoUidAllowlist == null) {
357                         mNoUidAllowlist =
358                                 getListLocked(PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST);
359                         if (mNoUidAllowlist == null) {
360                             // failed to get the allowlist. Block it.
361                             return LOGGING_LEVEL_BLOCKED;
362                         }
363                     }
364                     Integer level = loggingLevelInternal(
365                             packages,
366                             mNoUidAllowlist,
367                             PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST);
368                     if (level != null) {
369                         return level;
370                     }
371                     if (mAllowlist == null) {
372                         mAllowlist = getListLocked(PLAYER_METRICS_APP_ALLOWLIST);
373                         if (mAllowlist == null) {
374                             // failed to get the allowlist. Block it.
375                             return LOGGING_LEVEL_BLOCKED;
376                         }
377                     }
378                     level = loggingLevelInternal(
379                             packages, mAllowlist, PLAYER_METRICS_APP_ALLOWLIST);
380                     if (level != null) {
381                         return level;
382                     }
383                     // Not detected in any allowlist. Block.
384                     return LOGGING_LEVEL_BLOCKED;
385                 }
386             }
387             // Blocked by default.
388             return LOGGING_LEVEL_BLOCKED;
389         }
390 
loggingLevelInternal( String[] packages, List<String> cached, String listName)391         private Integer loggingLevelInternal(
392                 String[] packages, List<String> cached, String listName) {
393             if (inList(packages, cached)) {
394                 return listNameToLoggingLevel(listName);
395             }
396             return null;
397         }
398 
inList(String[] packages, List<String> arr)399         private boolean inList(String[] packages, List<String> arr) {
400             for (String p : packages) {
401                 for (String element : arr) {
402                     if (p.equals(element)) {
403                         return true;
404                     }
405                 }
406             }
407             return false;
408         }
409 
listNameToLoggingLevel(String listName)410         private int listNameToLoggingLevel(String listName) {
411             switch (listName) {
412                 case PLAYER_METRICS_APP_BLOCKLIST:
413                     return LOGGING_LEVEL_BLOCKED;
414                 case PLAYER_METRICS_APP_ALLOWLIST:
415                     return LOGGING_LEVEL_EVERYTHING;
416                 case PLAYER_METRICS_PER_APP_ATTRIBUTION_ALLOWLIST:
417                 case PLAYER_METRICS_PER_APP_ATTRIBUTION_BLOCKLIST:
418                     return LOGGING_LEVEL_NO_UID;
419                 default:
420                     return LOGGING_LEVEL_BLOCKED;
421             }
422         }
423     }
424 }
425