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