/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.notification; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioAttributes; import android.media.AudioFocusRequest; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.UserHandle; import android.telephony.TelephonyManager; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import java.util.HashMap; /** * Helper class for playing notification beeps. For Feature_automotive the sounds for notification * will be disabled at the server level and notification center will handle playing all the sounds * using this class. */ class Beeper { private static final String TAG = "Beeper"; private static final long ALLOWED_ALERT_INTERVAL = 1000; private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG; private final Context mContext; private final AudioManager mAudioManager; private final Uri mInCallSoundToPlayUri; private AudioAttributes mPlaybackAttributes; private boolean mInCall; private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) { mInCall = TelephonyManager.EXTRA_STATE_OFFHOOK .equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE)); } } }; /** * Map that contains all the package name as the key for which the notifications made * noise. The value will be the last notification post time from the package. */ private final HashMap packageLastPostedTime; @Nullable private BeepRecord currentBeep; public Beeper(Context context) { mContext = context; mAudioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); mInCallSoundToPlayUri = Uri.parse("file://" + context.getResources().getString( com.android.internal.R.string.config_inCallNotificationSound)); packageLastPostedTime = new HashMap<>(); IntentFilter filter = new IntentFilter(); filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); context.registerReceiver(mIntentReceiver, filter); } /** * Beep with a provided sound. * * @param packageName of which {@link AlertEntry} belongs to. * @param soundToPlay {@link Uri} from where the sound will be played. */ @MainThread public void beep(String packageName, Uri soundToPlay) { if (!canAlert(packageName)) { if (DEBUG) { Log.d(TAG, "Package recently made noise: " + packageName); } return; } packageLastPostedTime.put(packageName, System.currentTimeMillis()); stopBeeping(); if (mInCall) { currentBeep = new BeepRecord(mInCallSoundToPlayUri); } else { currentBeep = new BeepRecord(soundToPlay); } currentBeep.play(); } /** * Checks if the package is allowed to make noise or not. */ private boolean canAlert(String packageName) { if (packageLastPostedTime.containsKey(packageName)) { long lastPostedTime = packageLastPostedTime.get(packageName); return System.currentTimeMillis() - lastPostedTime > ALLOWED_ALERT_INTERVAL; } return true; } @MainThread void stopBeeping() { if (currentBeep != null) { currentBeep.stop(); currentBeep = null; } } /** A class that represents a beep through its lifecycle. */ private final class BeepRecord implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, AudioManager.OnAudioFocusChangeListener { private final Uri mBeepUri; private final int mBeepStream; private final MediaPlayer mPlayer; /** Only set in case of an error. See {@link #playViaRingtoneManager}. */ @Nullable private Ringtone mRingtone; private int mAudiofocusRequestFailed = AudioManager.AUDIOFOCUS_REQUEST_FAILED; private boolean mCleanedUp; /** * Create a new {@link BeepRecord} that will play the given sound. * * @param beepUri The sound to play. */ public BeepRecord(Uri beepUri) { this.mBeepUri = beepUri; this.mBeepStream = AudioManager.STREAM_MUSIC; mPlayer = new MediaPlayer(); mPlayer.setOnPreparedListener(this); mPlayer.setOnCompletionListener(this); mPlayer.setOnErrorListener(this); } /** Start playing the sound. */ @MainThread public void play() { if (DEBUG) { Log.d(TAG, "playing sound: "); } try { mPlayer.setDataSource(getContextForForegroundUser(), mBeepUri, /* headers= */null); mPlaybackAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); mPlayer.setAudioAttributes(mPlaybackAttributes); mPlayer.prepareAsync(); } catch (Exception e) { Log.d(TAG, "playing via ringtone manager: " + e); handleError(); } } /** Stop the currently playing sound, if it's playing. If it isn't, do nothing. */ @MainThread public void stop() { if (!mCleanedUp && mPlayer.isPlaying()) { mPlayer.stop(); } if (mRingtone != null) { mRingtone.stop(); mRingtone = null; } cleanUp(); } /** Handle MediaPlayer preparation completing - gain audio focus and play the sound. */ @Override // MediaPlayer.OnPreparedListener public void onPrepared(MediaPlayer mediaPlayer) { if (mCleanedUp) { return; } AudioFocusRequest focusRequest = new AudioFocusRequest.Builder( AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) .setAudioAttributes(mPlaybackAttributes) .setOnAudioFocusChangeListener(this, new Handler()) .build(); mAudiofocusRequestFailed = mAudioManager.requestAudioFocus(focusRequest); if (mAudiofocusRequestFailed == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // Only play the sound if we actually gained audio focus. mPlayer.start(); } else { cleanUp(); } } /** Handle completion by cleaning up our state. */ @Override // MediaPlayer.OnCompletionListener public void onCompletion(MediaPlayer mediaPlayer) { cleanUp(); } /** Handle errors that come from MediaPlayer. */ @Override // MediaPlayer.OnErrorListener public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { handleError(); return true; } /** * Not actually used for anything, but allows us to pass {@code this} to {@link * AudioManager#requestAudioFocus}, so that different audio focus requests from different * {@link BeepRecord}s don't collide. */ @Override // AudioManager.OnAudioFocusChangeListener public void onAudioFocusChange(int i) { } /** * Notifications is running in the system process, so we want to make sure we lookup sounds * in the foreground user's space. */ private Context getContextForForegroundUser() { try { return mContext.createPackageContextAsUser(mContext.getPackageName(), /* flags= */ 0, UserHandle.of(ActivityManager.getCurrentUser())); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } } /** Handle an error by trying to play the sound through {@link RingtoneManager}. */ private void handleError() { cleanUp(); playViaRingtoneManager(); } /** Clean up and release our state. */ private void cleanUp() { if (mAudiofocusRequestFailed == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { mAudioManager.abandonAudioFocus(this); mAudiofocusRequestFailed = AudioManager.AUDIOFOCUS_REQUEST_FAILED; } mPlayer.release(); mCleanedUp = true; } /** * Handle a failure to play the sound directly, by playing through {@link RingtoneManager}. * *

RingtoneManager is equipped to play sounds that require READ_EXTERNAL_STORAGE * permission (see b/30572189), but can't handle requesting and releasing audio focus. * Since we want audio focus in the common case, try playing the sound ourselves through * MediaPlayer before we give up and hand over to RingtoneManager. */ private void playViaRingtoneManager() { mRingtone = RingtoneManager.getRingtone(getContextForForegroundUser(), mBeepUri); if (mRingtone != null) { mRingtone.setStreamType(mBeepStream); mRingtone.play(); } } } }