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