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