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