• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2014, 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.server.telecom;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.media.Ringtone;
22 import android.media.VolumeShaper;
23 import android.net.Uri;
24 import android.os.Handler;
25 import android.os.HandlerThread;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.telecom.Log;
29 import android.telecom.Logging.Session;
30 import android.util.Pair;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.os.SomeArgs;
34 import com.android.internal.util.Preconditions;
35 
36 import java.util.ArrayList;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 import java.util.function.BiConsumer;
40 import java.util.function.Supplier;
41 
42 /**
43  * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
44  * used from the main thread.
45  */
46 @VisibleForTesting
47 public class AsyncRingtonePlayer {
48     // Maximum amount of time we will delay playing a ringtone while waiting for audio routing to
49     // be ready.
50     private static final int PLAY_DELAY_TIMEOUT_MS = 1000;
51     // Message codes used with the ringtone thread.
52     private static final int EVENT_PLAY = 1;
53     private static final int EVENT_STOP = 2;
54 
55     /** Handler running on the ringtone thread. */
56     private Handler mHandler;
57 
58     /** The current ringtone. Only used by the ringtone thread. */
59     private Ringtone mRingtone;
60 
61     /**
62      * Set to true if we are setting up to play or are currently playing. False if we are stopping
63      * or have stopped playing.
64      */
65     private boolean mIsPlaying = false;
66 
67     /**
68      * Set to true if BT HFP is active and audio is connected.
69      */
70     private boolean mIsBtActive = false;
71 
72     /**
73      * A list of pending ringing ready latches, which are used to delay the ringing command until
74      * audio paths are set and ringing is ready.
75      */
76     private final ArrayList<CountDownLatch> mPendingRingingLatches = new ArrayList<>();
77 
AsyncRingtonePlayer()78     public AsyncRingtonePlayer() {
79         // Empty
80     }
81 
82     /**
83      * Plays the appropriate ringtone for the specified call.
84      * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
85      * the volume of the ringtone as it plays.
86      *
87      * @param ringtoneInfoSupplier The {@link Ringtone} factory.
88      * @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
89      * @param isHfpDeviceConnected True if there is a HFP BT device connected, false otherwise.
90      */
play(@onNull Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier, BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer, boolean isHfpDeviceConnected)91     public void play(@NonNull Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier,
92             BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer,
93             boolean isHfpDeviceConnected) {
94         Log.d(this, "Posting play.");
95         mIsPlaying = true;
96         SomeArgs args = SomeArgs.obtain();
97         args.arg1 = ringtoneInfoSupplier;
98         args.arg2 = ringtoneConsumer;
99         args.arg3 = Log.createSubsession();
100         args.arg4 = prepareRingingReadyLatch(isHfpDeviceConnected);
101         postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
102     }
103 
104     /** Stops playing the ringtone. */
stop()105     public void stop() {
106         Log.d(this, "Posting stop.");
107         mIsPlaying = false;
108         postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
109         // Clear any pending ringing latches so that we do not have to wait for its timeout to pass
110         // before calling stop.
111         clearPendingRingingLatches();
112     }
113 
114     /**
115      * Called when the BT HFP profile active state changes.
116      * @param isBtActive A BT device is connected and audio is active.
117      */
updateBtActiveState(boolean isBtActive)118     public void updateBtActiveState(boolean isBtActive) {
119         Log.i(this, "updateBtActiveState: " + isBtActive);
120         synchronized (mPendingRingingLatches) {
121             mIsBtActive = isBtActive;
122             if (isBtActive) mPendingRingingLatches.forEach(CountDownLatch::countDown);
123         }
124     }
125 
126     /**
127      * Prepares a new ringing ready latch and tracks it in a list. Once the ready latch has been
128      * used, {@link #removePendingRingingReadyLatch(CountDownLatch)} must be called on this latch.
129      * @param isHfpDeviceConnected true if there is a HFP device connected.
130      * @return the newly prepared CountDownLatch
131      */
prepareRingingReadyLatch(boolean isHfpDeviceConnected)132     private CountDownLatch prepareRingingReadyLatch(boolean isHfpDeviceConnected) {
133         CountDownLatch latch = new CountDownLatch(1);
134         synchronized (mPendingRingingLatches) {
135             // We only want to delay ringing if BT is connected but not active yet.
136             boolean isDelayRequired = isHfpDeviceConnected && !mIsBtActive;
137             Log.i(this, "prepareRingingReadyLatch:"
138                     + " connected=" + isHfpDeviceConnected
139                     + ", BT active=" + mIsBtActive
140                     + ", isDelayRequired=" + isDelayRequired);
141             if (!isDelayRequired) latch.countDown();
142             mPendingRingingLatches.add(latch);
143         }
144         return latch;
145     }
146 
147     /**
148      * Remove a ringing ready latch that has been used and is no longer pending.
149      * @param l The latch to remove.
150      */
removePendingRingingReadyLatch(CountDownLatch l)151     private void removePendingRingingReadyLatch(CountDownLatch l) {
152         synchronized (mPendingRingingLatches) {
153             mPendingRingingLatches.remove(l);
154         }
155     }
156 
157     /**
158      * Count down all pending ringing ready latches and then clear the list.
159      */
clearPendingRingingLatches()160     private void clearPendingRingingLatches() {
161         synchronized (mPendingRingingLatches) {
162             mPendingRingingLatches.forEach(CountDownLatch::countDown);
163             mPendingRingingLatches.clear();
164         }
165     }
166 
167     /**
168      * Posts a message to the ringtone-thread handler. Creates the handler if specified by the
169      * parameter shouldCreateHandler.
170      *
171      * @param messageCode The message to post.
172      * @param shouldCreateHandler True when a handler should be created to handle this message.
173      */
postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args)174     private void postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args) {
175         synchronized(this) {
176             if (mHandler == null && shouldCreateHandler) {
177                 mHandler = getNewHandler();
178             }
179 
180             if (mHandler == null) {
181                 Log.d(this, "Message %d skipped because there is no handler.", messageCode);
182             } else {
183                 mHandler.obtainMessage(messageCode, args).sendToTarget();
184             }
185         }
186     }
187 
getLooper()188     public @NonNull Looper getLooper() {
189         if (mHandler == null) {
190             mHandler = getNewHandler();
191         }
192         return mHandler.getLooper();
193     }
194 
195     /**
196      * Creates a new ringtone Handler running in its own thread.
197      */
getNewHandler()198     private Handler getNewHandler() {
199         Preconditions.checkState(mHandler == null);
200 
201         HandlerThread thread = new HandlerThread("ringtone-player");
202         thread.start();
203 
204         return new Handler(thread.getLooper(), null /*callback*/, true /*async*/) {
205             @Override
206             public void handleMessage(Message msg) {
207                 switch(msg.what) {
208                     case EVENT_PLAY:
209                         handlePlay((SomeArgs) msg.obj);
210                         break;
211                     case EVENT_STOP:
212                         handleStop();
213                         break;
214                 }
215             }
216         };
217     }
218 
219     /**
220      * Starts the actual playback of the ringtone. Executes on ringtone-thread.
221      */
222     private void handlePlay(SomeArgs args) {
223         Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier =
224                 (Supplier<Pair<Uri, Ringtone>>) args.arg1;
225         BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer =
226                 (BiConsumer<Pair<Uri, Ringtone>, Boolean>) args.arg2;
227         Session session = (Session) args.arg3;
228         CountDownLatch ringingReadyLatch = (CountDownLatch) args.arg4;
229         args.recycle();
230 
231         Log.continueSession(session, "ARP.hP");
232         try {
233             // Don't bother with any of this if there is an EVENT_STOP waiting, but give the
234             // consumer a chance to do anything no matter what.
235             if (mHandler.hasMessages(EVENT_STOP)) {
236                 Log.i(this, "handlePlay: skipping play early due to pending STOP");
237                 removePendingRingingReadyLatch(ringingReadyLatch);
238                 ringtoneConsumer.accept(null, /* stopped= */ true);
239                 return;
240             }
241             Ringtone ringtone = null;
242             Uri ringtoneUri = null;
243             boolean hasStopped = false;
244             try {
245                 try {
246                     Log.i(this, "handlePlay: delay ring for ready signal...");
247                     boolean reachedZero = ringingReadyLatch.await(PLAY_DELAY_TIMEOUT_MS,
248                             TimeUnit.MILLISECONDS);
249                     Log.i(this, "handlePlay: ringing ready, timeout=" + !reachedZero);
250                 } catch (InterruptedException e) {
251                     Log.w(this, "handlePlay: latch exception: " + e);
252                 }
253                 if (ringtoneInfoSupplier != null && ringtoneInfoSupplier.get() != null) {
254                     ringtoneUri = ringtoneInfoSupplier.get().first;
255                     ringtone = ringtoneInfoSupplier.get().second;
256                 }
257 
258                 // Ringtone supply can be slow or stop command could have been issued while waiting
259                 // for BT to move to CONNECTED state. Re-check for stop event.
260                 if (mHandler.hasMessages(EVENT_STOP)) {
261                     Log.i(this, "handlePlay: skipping play due to pending STOP");
262                     hasStopped = true;
263                     if (ringtone != null) ringtone.stop();  // proactively release the ringtone.
264                     return;
265                 }
266                 // setRingtone even if null - it also stops any current ringtone to be consistent
267                 // with the overall state.
268                 setRingtone(ringtone);
269                 if (mRingtone == null) {
270                     // The ringtoneConsumer can still vibrate at this stage.
271                     Log.w(this, "No ringtone was found bail out from playing.");
272                     return;
273                 }
274                 String uriString = ringtoneUri != null ? ringtoneUri.toSafeString() : "";
275                 Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
276                 mRingtone.setLooping(true);
277                 if (mRingtone.isPlaying()) {
278                     Log.d(this, "Ringtone already playing.");
279                     return;
280                 }
281                 mRingtone.play();
282                 Log.i(this, "Play ringtone, looping.");
283             } finally {
284                 removePendingRingingReadyLatch(ringingReadyLatch);
285                 ringtoneConsumer.accept(new Pair(ringtoneUri, ringtone), hasStopped);
286             }
287         } finally {
288             Log.cancelSubsession(session);
289         }
290     }
291 
292     /**
293      * Stops the playback of the ringtone. Executes on the ringtone-thread.
294      */
295     private void handleStop() {
296         ThreadUtil.checkNotOnMainThread();
297         Log.i(this, "Stop ringtone.");
298 
299         setRingtone(null);
300 
301         synchronized(this) {
302             if (mHandler.hasMessages(EVENT_PLAY)) {
303                 Log.v(this, "Keeping alive ringtone thread for subsequent play request.");
304             } else {
305                 mHandler.removeMessages(EVENT_STOP);
306                 mHandler.getLooper().quitSafely();
307                 mHandler = null;
308                 Log.v(this, "Handler cleared.");
309             }
310         }
311     }
312 
313     /**
314      * @return true if we are currently preparing or playing a ringtone, false if we are not.
315      */
316     public boolean isPlaying() {
317         return mIsPlaying;
318     }
319 
320     private void setRingtone(@Nullable Ringtone ringtone) {
321         Log.i(this, "setRingtone: ringtone null="  + (ringtone == null));
322         // Make sure that any previously created instance of Ringtone is stopped so the MediaPlayer
323         // can be released, before replacing mRingtone with a new instance. This is always created
324         // as a looping Ringtone, so if not stopped it will keep playing on the background.
325         if (mRingtone != null) {
326             Log.d(this, "Ringtone.stop() invoked.");
327             mRingtone.stop();
328         }
329         mRingtone = ringtone;
330     }
331 }
332