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