1 /* 2 * Copyright (C) 2019 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.car.notification; 18 19 import android.car.userlib.CarUserManagerHelper; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.pm.PackageManager; 25 import android.media.AudioAttributes; 26 import android.media.AudioFocusRequest; 27 import android.media.AudioManager; 28 import android.media.MediaPlayer; 29 import android.media.Ringtone; 30 import android.media.RingtoneManager; 31 import android.net.Uri; 32 import android.os.Handler; 33 import android.os.UserHandle; 34 import android.service.notification.StatusBarNotification; 35 import android.telephony.TelephonyManager; 36 import android.util.Log; 37 38 import androidx.annotation.MainThread; 39 import androidx.annotation.Nullable; 40 41 import java.util.HashMap; 42 43 /** 44 * Helper class for playing notification beeps. For Feature_automotive the sounds for notification 45 * will be disabled at the server level and notification center will handle playing all the sounds 46 * using this class. 47 */ 48 class Beeper { 49 private static final String TAG = "Beeper"; 50 private static final long ALLOWED_ALERT_INTERVAL = 1000; 51 private static final boolean DEBUG = false; 52 53 private final Context mContext; 54 private final AudioManager mAudioManager; 55 private final Uri mInCallSoundToPlayUri; 56 private final CarUserManagerHelper mCarUserManagerHelper; 57 private AudioAttributes mPlaybackAttributes; 58 59 private boolean mInCall; 60 61 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 62 @Override 63 public void onReceive(Context context, Intent intent) { 64 String action = intent.getAction(); 65 if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) { 66 mInCall = TelephonyManager.EXTRA_STATE_OFFHOOK 67 .equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE)); 68 } 69 } 70 }; 71 72 /** 73 * Map that contains all the package name as the key for which the notifications made 74 * noise. The value will be the last notification post time from the package. 75 */ 76 private final HashMap<String, Long> packageLastPostedTime; 77 78 @Nullable 79 private BeepRecord currentBeep; 80 Beeper(Context context)81 public Beeper(Context context) { 82 this.mContext = context; 83 mAudioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); 84 mInCallSoundToPlayUri = Uri.parse("file://" + context.getResources().getString( 85 com.android.internal.R.string.config_inCallNotificationSound)); 86 mCarUserManagerHelper = new CarUserManagerHelper(context); 87 packageLastPostedTime = new HashMap<>(); 88 IntentFilter filter = new IntentFilter(); 89 filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); 90 context.registerReceiver(mIntentReceiver, filter); 91 } 92 93 /** 94 * Beep with a provided sound. 95 * 96 * @param packageName of which {@link StatusBarNotification} belongs to. 97 * @param soundToPlay {@link Uri} from where the sound will be played. 98 */ 99 @MainThread beep(String packageName, Uri soundToPlay)100 public void beep(String packageName, Uri soundToPlay) { 101 if (!canAlert(packageName)) { 102 if (DEBUG) { 103 Log.d(TAG, "Package recently made noise: " + packageName); 104 } 105 return; 106 } 107 108 packageLastPostedTime.put(packageName, System.currentTimeMillis()); 109 stopBeeping(); 110 if (mInCall) { 111 currentBeep = new BeepRecord(mInCallSoundToPlayUri); 112 } else { 113 currentBeep = new BeepRecord(soundToPlay); 114 } 115 currentBeep.play(); 116 } 117 118 /** 119 * Checks if the package is allowed to make noise or not. 120 */ canAlert(String packageName)121 private boolean canAlert(String packageName) { 122 if (packageLastPostedTime.containsKey(packageName)) { 123 long lastPostedTime = packageLastPostedTime.get(packageName); 124 return System.currentTimeMillis() - lastPostedTime > ALLOWED_ALERT_INTERVAL; 125 } 126 return true; 127 } 128 129 @MainThread stopBeeping()130 void stopBeeping() { 131 if (currentBeep != null) { 132 currentBeep.stop(); 133 currentBeep = null; 134 } 135 } 136 137 /** A class that represents a beep through its lifecycle. */ 138 private final class BeepRecord implements MediaPlayer.OnPreparedListener, 139 MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, 140 AudioManager.OnAudioFocusChangeListener { 141 142 private final Uri mBeepUri; 143 private final int mBeepStream; 144 private final MediaPlayer mPlayer; 145 146 /** Only set in case of an error. See {@link #playViaRingtoneManager}. */ 147 @Nullable 148 private Ringtone mRingtone; 149 150 private int mAudiofocusRequestFailed = AudioManager.AUDIOFOCUS_REQUEST_FAILED; 151 private boolean mCleanedUp; 152 153 /** 154 * Create a new {@link BeepRecord} that will play the given sound. 155 * 156 * @param beepUri The sound to play. 157 */ BeepRecord(Uri beepUri)158 public BeepRecord(Uri beepUri) { 159 this.mBeepUri = beepUri; 160 this.mBeepStream = AudioManager.STREAM_MUSIC; 161 mPlayer = new MediaPlayer(); 162 mPlayer.setOnPreparedListener(this); 163 mPlayer.setOnCompletionListener(this); 164 mPlayer.setOnErrorListener(this); 165 } 166 167 /** Start playing the sound. */ 168 @MainThread play()169 public void play() { 170 if (DEBUG) { 171 Log.d(TAG, "playing sound: "); 172 } 173 try { 174 mPlayer.setDataSource(getContextForForegroundUser(), mBeepUri); 175 mPlaybackAttributes = new AudioAttributes.Builder() 176 .setUsage(AudioAttributes.USAGE_NOTIFICATION) 177 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 178 .build(); 179 mPlayer.setAudioAttributes(mPlaybackAttributes); 180 mPlayer.prepareAsync(); 181 } catch (Exception e) { 182 Log.d(TAG, "playing via ringtone manager: " + e); 183 handleError(); 184 } 185 } 186 187 /** Stop the currently playing sound, if it's playing. If it isn't, do nothing. */ 188 @MainThread stop()189 public void stop() { 190 if (!mCleanedUp && mPlayer.isPlaying()) { 191 mPlayer.stop(); 192 } 193 194 if (mRingtone != null) { 195 mRingtone.stop(); 196 mRingtone = null; 197 } 198 cleanUp(); 199 } 200 201 /** Handle MediaPlayer preparation completing - gain audio focus and play the sound. */ 202 @Override // MediaPlayer.OnPreparedListener onPrepared(MediaPlayer mediaPlayer)203 public void onPrepared(MediaPlayer mediaPlayer) { 204 if (mCleanedUp) { 205 return; 206 } 207 AudioFocusRequest focusRequest = new AudioFocusRequest.Builder( 208 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) 209 .setAudioAttributes(mPlaybackAttributes) 210 .setOnAudioFocusChangeListener(this, new Handler()) 211 .build(); 212 213 mAudiofocusRequestFailed = mAudioManager.requestAudioFocus(focusRequest); 214 if (mAudiofocusRequestFailed == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 215 // Only play the sound if we actually gained audio focus. 216 mPlayer.start(); 217 } else { 218 cleanUp(); 219 } 220 } 221 222 /** Handle completion by cleaning up our state. */ 223 @Override // MediaPlayer.OnCompletionListener onCompletion(MediaPlayer mediaPlayer)224 public void onCompletion(MediaPlayer mediaPlayer) { 225 cleanUp(); 226 } 227 228 /** Handle errors that come from MediaPlayer. */ 229 @Override // MediaPlayer.OnErrorListener onError(MediaPlayer mediaPlayer, int what, int extra)230 public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { 231 handleError(); 232 return true; 233 } 234 235 /** 236 * Not actually used for anything, but allows us to pass {@code this} to {@link 237 * AudioManager#requestAudioFocus}, so that different audio focus requests from different 238 * {@link BeepRecord}s don't collide. 239 */ 240 @Override // AudioManager.OnAudioFocusChangeListener onAudioFocusChange(int i)241 public void onAudioFocusChange(int i) { 242 } 243 244 /** 245 * Notifications is running in the system process, so we want to make sure we lookup sounds 246 * in the foreground user's space. 247 */ getContextForForegroundUser()248 private Context getContextForForegroundUser() { 249 try { 250 return mContext.createPackageContextAsUser(mContext.getPackageName(), /* flags= */ 251 0, UserHandle.of(mCarUserManagerHelper.getCurrentForegroundUserId())); 252 } catch (PackageManager.NameNotFoundException e) { 253 throw new RuntimeException(e); 254 } 255 } 256 257 /** Handle an error by trying to play the sound through {@link RingtoneManager}. */ handleError()258 private void handleError() { 259 cleanUp(); 260 playViaRingtoneManager(); 261 } 262 263 /** Clean up and release our state. */ cleanUp()264 private void cleanUp() { 265 if (mAudiofocusRequestFailed == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 266 mAudioManager.abandonAudioFocus(this); 267 mAudiofocusRequestFailed = AudioManager.AUDIOFOCUS_REQUEST_FAILED; 268 } 269 mPlayer.release(); 270 mCleanedUp = true; 271 } 272 273 /** 274 * Handle a failure to play the sound directly, by playing through {@link RingtoneManager}. 275 * 276 * <p>RingtoneManager is equipped to play sounds that require READ_EXTERNAL_STORAGE 277 * permission (see b/30572189), but can't handle requesting and releasing audio focus. 278 * Since we want audio focus in the common case, try playing the sound ourselves through 279 * MediaPlayer before we give up and hand over to RingtoneManager. 280 */ playViaRingtoneManager()281 private void playViaRingtoneManager() { 282 mRingtone = RingtoneManager.getRingtone(getContextForForegroundUser(), mBeepUri); 283 if (mRingtone != null) { 284 mRingtone.setStreamType(mBeepStream); 285 mRingtone.play(); 286 } 287 } 288 } 289 } 290