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