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