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