• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2023 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 package com.android.server.audio;
17 
18 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
19 
20 import android.Manifest;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.ActivityManager;
24 import android.app.AppOpsManager;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.media.AudioFocusRequest;
28 import android.media.AudioManager;
29 import android.os.Binder;
30 import android.os.Build;
31 import android.os.UserHandle;
32 import android.text.TextUtils;
33 import android.util.Slog;
34 import android.util.SparseArray;
35 
36 import com.android.modules.expresslog.Counter;
37 import com.android.server.utils.EventLogger;
38 
39 import java.io.PrintWriter;
40 import java.util.concurrent.atomic.AtomicBoolean;
41 
42 /**
43  * Class to encapsulate all audio API hardening operations
44  */
45 public class HardeningEnforcer {
46 
47     private static final String TAG = "AS.HardeningEnforcer";
48     private static final boolean DEBUG = false;
49     private static final int LOG_NB_EVENTS = 20;
50 
51     final Context mContext;
52     final AppOpsManager mAppOps;
53     final AtomicBoolean mShouldEnableAllHardening;
54     final boolean mIsAutomotive;
55 
56     final ActivityManager mActivityManager;
57     final PackageManager mPackageManager;
58 
59     final EventLogger mEventLogger;
60 
61     // capacity = 4 for each of the focus request types
62     static final SparseArray<String> METRIC_COUNTERS_FOCUS_DENIAL = new SparseArray<>(4);
63     static final SparseArray<String> METRIC_COUNTERS_FOCUS_GRANT = new SparseArray<>(4);
64 
65     static {
METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN, "media_audio.value_audio_focus_gain_granted")66         METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN,
67                 "media_audio.value_audio_focus_gain_granted");
METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, "media_audio.value_audio_focus_gain_transient_granted")68         METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
69                 "media_audio.value_audio_focus_gain_transient_granted");
METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, "media_audio.value_audio_focus_gain_transient_duck_granted")70         METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
71                 "media_audio.value_audio_focus_gain_transient_duck_granted");
METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, "media_audio.value_audio_focus_gain_transient_excl_granted")72         METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
73                 "media_audio.value_audio_focus_gain_transient_excl_granted");
74 
METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN, "media_audio.value_audio_focus_gain_appops_denial")75         METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN,
76                 "media_audio.value_audio_focus_gain_appops_denial");
METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, "media_audio.value_audio_focus_gain_transient_appops_denial")77         METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
78                 "media_audio.value_audio_focus_gain_transient_appops_denial");
METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, "media_audio.value_audio_focus_gain_transient_duck_appops_denial")79         METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
80                 "media_audio.value_audio_focus_gain_transient_duck_appops_denial");
METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, "media_audio.value_audio_focus_gain_transient_excl_appops_denial")81         METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
82                 "media_audio.value_audio_focus_gain_transient_excl_appops_denial");
83     }
84 
85     /**
86      * Matches calls from {@link AudioManager#setStreamVolume(int, int, int)}
87      */
88     public static final int METHOD_AUDIO_MANAGER_SET_STREAM_VOLUME = 100;
89     /**
90      * Matches calls from {@link AudioManager#adjustVolume(int, int)}
91      */
92     public static final int METHOD_AUDIO_MANAGER_ADJUST_VOLUME = 101;
93     /**
94      * Matches calls from {@link AudioManager#adjustSuggestedStreamVolume(int, int, int)}
95      */
96     public static final int METHOD_AUDIO_MANAGER_ADJUST_SUGGESTED_STREAM_VOLUME = 102;
97     /**
98      * Matches calls from {@link AudioManager#adjustStreamVolume(int, int, int)}
99      */
100     public static final int METHOD_AUDIO_MANAGER_ADJUST_STREAM_VOLUME = 103;
101     /**
102      * Matches calls from {@link AudioManager#setRingerMode(int)}
103      */
104     public static final int METHOD_AUDIO_MANAGER_SET_RINGER_MODE = 200;
105     /**
106      * Matches calls from {@link AudioManager#requestAudioFocus(AudioFocusRequest)}
107      * and legacy variants
108      */
109     public static final int METHOD_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS = 300;
110 
111     private static final int ALLOWED = 0;
112     private static final int DENIED_IF_PARTIAL = 1;
113     private static final int DENIED_IF_FULL = 2;
114 
HardeningEnforcer(Context ctxt, boolean isAutomotive, AtomicBoolean shouldEnableHardening, AppOpsManager appOps, PackageManager pm, EventLogger logger)115     public HardeningEnforcer(Context ctxt, boolean isAutomotive,
116             AtomicBoolean shouldEnableHardening, AppOpsManager appOps, PackageManager pm,
117             EventLogger logger) {
118         mContext = ctxt;
119         mIsAutomotive = isAutomotive;
120         mShouldEnableAllHardening = shouldEnableHardening;
121         mAppOps = appOps;
122         mActivityManager = ctxt.getSystemService(ActivityManager.class);
123         mPackageManager = pm;
124         mEventLogger = logger;
125     }
126 
127     /**
128      * Checks whether the call in the current thread should be allowed or blocked
129      * @param volumeMethod name of the method to check, for logging purposes
130      * @return false if the method call is allowed, true if it should be a no-op
131      */
blockVolumeMethod(int volumeMethod, String packageName, int uid)132     protected boolean blockVolumeMethod(int volumeMethod, String packageName, int uid) {
133         // Regardless of flag state, always permit callers with MODIFY_AUDIO_SETTINGS_PRIVILEGED
134         // Prevent them from showing up in metrics as well
135         if (mContext.checkCallingOrSelfPermission(
136                 Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
137                 == PackageManager.PERMISSION_GRANTED) {
138             return false;
139         }
140         // for Auto, volume methods require MODIFY_AUDIO_SETTINGS_PRIVILEGED
141         if (mIsAutomotive) {
142             if (!autoPublicVolumeApiHardening()) {
143                 // automotive hardening flag disabled, no blocking on auto
144                 return false;
145             }
146             if (uid < UserHandle.AID_APP_START) {
147                 return false;
148             }
149             // TODO metrics?
150             // TODO log for audio dumpsys?
151             Slog.e(TAG, "Preventing volume method " + volumeMethod + " for "
152                     + packageName);
153             return true;
154         } else {
155             int allowed;
156             // No flags controlling restriction yet
157             boolean enforced = mShouldEnableAllHardening.get();
158             if (!noteOp(AppOpsManager.OP_CONTROL_AUDIO_PARTIAL, uid, packageName, null)) {
159                 // blocked by partial
160                 Counter.logIncrementWithUid(
161                         "media_audio.value_audio_volume_hardening_partial_restriction", uid);
162                 allowed = DENIED_IF_PARTIAL;
163             } else if (!noteOp(AppOpsManager.OP_CONTROL_AUDIO, uid, packageName, null)) {
164                 // blocked by full, permitted by partial
165                 Counter.logIncrementWithUid(
166                         "media_audio.value_audio_volume_hardening_strict_restriction", uid);
167                 allowed = DENIED_IF_FULL;
168             } else {
169                 // permitted with strict hardening, log anyway for API metrics
170                 Counter.logIncrementWithUid(
171                         "media_audio.value_audio_volume_hardening_allowed", uid);
172                 allowed = ALLOWED;
173             }
174             if (allowed != ALLOWED) {
175                 String msg = "AudioHardening volume control for api "
176                         + volumeMethod
177                         + (!enforced ? " would be " : " ")
178                         + "ignored for "
179                         + getPackNameForUid(uid) + " (" + uid + "), "
180                         + "level: " + (allowed == DENIED_IF_PARTIAL ? "partial" : "full");
181                 mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG);
182             }
183             return enforced && allowed != ALLOWED;
184         }
185     }
186 
187     /**
188      * Checks whether the call in the current thread should be allowed or blocked
189      * @param focusMethod name of the method to check, for logging purposes
190      * @param clientId id of the requester
191      * @param focusReqType focus type being requested
192      * @param attributionTag attribution of the caller
193      * @param targetSdk target SDK of the caller
194      * @return false if the method call is allowed, true if it should be a no-op
195      */
196     @SuppressWarnings("AndroidFrameworkCompatChange")
blockFocusMethod(int callingUid, int focusMethod, @NonNull String clientId, int focusReqType, @NonNull String packageName, String attributionTag, int targetSdk)197     protected boolean blockFocusMethod(int callingUid, int focusMethod, @NonNull String clientId,
198             int focusReqType, @NonNull String packageName, String attributionTag, int targetSdk) {
199         if (packageName.isEmpty()) {
200             packageName = getPackNameForUid(callingUid);
201         }
202         // indicates would be blocked if audio capabilities were required
203         boolean blockedIfFull = !noteOp(AppOpsManager.OP_CONTROL_AUDIO,
204                                         callingUid, packageName, attributionTag);
205         boolean blocked = true;
206         // indicates the focus request was not blocked because of the SDK version
207         boolean unblockedBySdk = false;
208         if (noteOp(AppOpsManager.OP_TAKE_AUDIO_FOCUS, callingUid, packageName, attributionTag)) {
209             if (DEBUG) {
210                 Slog.i(TAG, "blockFocusMethod pack:" + packageName + " NOT blocking");
211             }
212             blocked = false;
213         } else if (targetSdk < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
214             if (DEBUG) {
215                 Slog.i(TAG, "blockFocusMethod pack:" + packageName + " NOT blocking due to sdk="
216                         + targetSdk);
217             }
218             unblockedBySdk = true;
219         }
220 
221         boolean enforced = mShouldEnableAllHardening.get() || !unblockedBySdk;
222         boolean enforcedFull = mShouldEnableAllHardening.get();
223 
224         metricsLogFocusReq(blocked && enforced, focusReqType, callingUid, unblockedBySdk);
225 
226         if (blocked) {
227             String msg = "AudioHardening focus request for req "
228                     + focusReqType
229                     + (!enforced ? " would be " : " ")
230                     + "ignored for "
231                     + packageName + " (" + callingUid + "), "
232                     + clientId
233                     + ", level: partial";
234             mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG);
235         } else if (blockedIfFull) {
236             String msg = "AudioHardening focus request for req "
237                     + focusReqType
238                     + (!enforcedFull ? " would be " : " ")
239                     + "ignored for "
240                     + packageName + " (" + callingUid + "), "
241                     + clientId
242                     + ", level: full";
243             mEventLogger.enqueueAndSlog(msg, EventLogger.Event.ALOGW, TAG);
244         }
245 
246         return blocked && enforced || (blockedIfFull && enforcedFull);
247     }
248 
249     /**
250      * Log metrics for the focus request
251      * @param blocked true if the call blocked
252      * @param focusReq the type of focus request
253      * @param callingUid the UID of the caller
254      * @param unblockedBySdk if blocked is false,
255      *                       true indicates it was unblocked thanks to an older SDK
256      */
metricsLogFocusReq(boolean blocked, int focusReq, int callingUid, boolean unblockedBySdk)257     /*package*/ void metricsLogFocusReq(boolean blocked, int focusReq, int callingUid,
258             boolean unblockedBySdk) {
259         final String metricId = blocked ? METRIC_COUNTERS_FOCUS_DENIAL.get(focusReq)
260                 : METRIC_COUNTERS_FOCUS_GRANT.get(focusReq);
261         if (TextUtils.isEmpty(metricId)) {
262             Slog.e(TAG, "Bad string for focus metrics gain:" + focusReq + " blocked:" + blocked);
263             return;
264         }
265         try {
266             Counter.logIncrementWithUid(metricId, callingUid);
267             if (!blocked && unblockedBySdk) {
268                 // additional metric to capture focus requests that are currently granted
269                 // because the app is on an older SDK, but would have been blocked otherwise
270                 Counter.logIncrementWithUid(
271                         "media_audio.value_audio_focus_grant_hardening_waived_by_sdk", callingUid);
272             }
273         } catch (Exception e) {
274             Slog.e(TAG, "Counter error metricId:" + metricId + " for focus req:" + focusReq
275                     + " from uid:" + callingUid, e);
276         }
277     }
278 
getPackNameForUid(int uid)279     private String getPackNameForUid(int uid) {
280         final long token = Binder.clearCallingIdentity();
281         try {
282             final String[] names = mPackageManager.getPackagesForUid(uid);
283             if (names == null
284                     || names.length == 0
285                     || TextUtils.isEmpty(names[0])) {
286                 return "[" + uid + "]";
287             }
288             return names[0];
289         } finally {
290             Binder.restoreCallingIdentity(token);
291         }
292     }
293 
294     /**
295      * Notes the given op without throwing
296      * @param op the appOp code
297      * @param uid the calling uid
298      * @param packageName the package name of the caller
299      * @param attributionTag attribution of the caller
300      * @return return false if the operation is not allowed
301      */
noteOp(int op, int uid, @NonNull String packageName, @Nullable String attributionTag)302     private boolean noteOp(int op, int uid, @NonNull String packageName,
303             @Nullable String attributionTag) {
304         if (mAppOps.noteOpNoThrow(op, uid, packageName, attributionTag, null)
305                 != AppOpsManager.MODE_ALLOWED) {
306             return false;
307         }
308         return true;
309     }
310 }
311