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.AudioAttributes; 22 import android.media.Ringtone; 23 import android.media.VolumeShaper; 24 import android.net.Uri; 25 import android.os.Handler; 26 import android.os.HandlerThread; 27 import android.os.Message; 28 import android.telecom.Log; 29 import android.telecom.Logging.Session; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.os.SomeArgs; 33 import com.android.internal.util.Preconditions; 34 35 import java.util.concurrent.CompletableFuture; 36 37 /** 38 * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be 39 * used from the main thread. 40 */ 41 @VisibleForTesting 42 public class AsyncRingtonePlayer { 43 // Message codes used with the ringtone thread. 44 private static final int EVENT_PLAY = 1; 45 private static final int EVENT_STOP = 2; 46 47 /** Handler running on the ringtone thread. */ 48 private Handler mHandler; 49 50 /** The current ringtone. Only used by the ringtone thread. */ 51 private Ringtone mRingtone; 52 53 /** 54 * CompletableFuture which signals a caller when we know whether a ringtone will play haptics 55 * or not. 56 */ 57 private CompletableFuture<Boolean> mHapticsFuture = null; 58 59 public AsyncRingtonePlayer() { 60 // Empty 61 } 62 63 /** 64 * Plays the appropriate ringtone for the specified call. 65 * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change 66 * the volume of the ringtone as it plays. 67 * 68 * @param factory The {@link RingtoneFactory}. 69 * @param incomingCall The ringing {@link Call}. 70 * @param volumeShaperConfig An optional {@link VolumeShaper.Configuration} which is applied to 71 * the ringtone to change its volume while it rings. 72 * @param isVibrationEnabled {@code true} if the settings and DND configuration of the device 73 * is such that the vibrator should be used, {@code false} otherwise. 74 * @return A {@link CompletableFuture} which on completion indicates whether or not the ringtone 75 * has a haptic track. {@code True} indicates that a haptic track is present on the 76 * ringtone; in this case the default vibration in {@link Ringer} should not be played. 77 * {@code False} indicates that a haptic track is NOT present on the ringtone; 78 * in this case the default vibration in {@link Ringer} should be trigger if needed. 79 */ 80 public @NonNull 81 CompletableFuture<Boolean> play(RingtoneFactory factory, Call incomingCall, 82 @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean isRingerAudible, 83 boolean isVibrationEnabled) { 84 Log.d(this, "Posting play."); 85 if (mHapticsFuture == null) { 86 mHapticsFuture = new CompletableFuture<>(); 87 } 88 SomeArgs args = SomeArgs.obtain(); 89 args.arg1 = factory; 90 args.arg2 = incomingCall; 91 args.arg3 = volumeShaperConfig; 92 args.arg4 = isVibrationEnabled; 93 args.arg5 = isRingerAudible; 94 args.arg6 = Log.createSubsession(); 95 postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args); 96 return mHapticsFuture; 97 } 98 99 /** Stops playing the ringtone. */ 100 public void stop() { 101 Log.d(this, "Posting stop."); 102 postMessage(EVENT_STOP, false /* shouldCreateHandler */, null); 103 } 104 105 /** 106 * Posts a message to the ringtone-thread handler. Creates the handler if specified by the 107 * parameter shouldCreateHandler. 108 * 109 * @param messageCode The message to post. 110 * @param shouldCreateHandler True when a handler should be created to handle this message. 111 */ 112 private void postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args) { 113 synchronized(this) { 114 if (mHandler == null && shouldCreateHandler) { 115 mHandler = getNewHandler(); 116 } 117 118 if (mHandler == null) { 119 Log.d(this, "Message %d skipped because there is no handler.", messageCode); 120 } else { 121 mHandler.obtainMessage(messageCode, args).sendToTarget(); 122 } 123 } 124 } 125 126 /** 127 * Creates a new ringtone Handler running in its own thread. 128 */ 129 private Handler getNewHandler() { 130 Preconditions.checkState(mHandler == null); 131 132 HandlerThread thread = new HandlerThread("ringtone-player"); 133 thread.start(); 134 135 return new Handler(thread.getLooper(), null /*callback*/, true /*async*/) { 136 @Override 137 public void handleMessage(Message msg) { 138 switch(msg.what) { 139 case EVENT_PLAY: 140 handlePlay((SomeArgs) msg.obj); 141 break; 142 case EVENT_STOP: 143 handleStop(); 144 break; 145 } 146 } 147 }; 148 } 149 150 /** 151 * Starts the actual playback of the ringtone. Executes on ringtone-thread. 152 */ 153 private void handlePlay(SomeArgs args) { 154 RingtoneFactory factory = (RingtoneFactory) args.arg1; 155 Call incomingCall = (Call) args.arg2; 156 VolumeShaper.Configuration volumeShaperConfig = (VolumeShaper.Configuration) args.arg3; 157 boolean isVibrationEnabled = (boolean) args.arg4; 158 boolean isRingerAudible = (boolean) args.arg5; 159 Session session = (Session) args.arg6; 160 args.recycle(); 161 162 Log.continueSession(session, "ARP.hP"); 163 try { 164 // don't bother with any of this if there is an EVENT_STOP waiting. 165 if (mHandler.hasMessages(EVENT_STOP)) { 166 completeHapticFuture(false /* ringtoneHasHaptics */); 167 return; 168 } 169 170 // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. 171 // If ringer is not audible for this call, then the phone is in "Vibrate" mode. 172 // Use haptic-only ringtone or do not play anything. 173 if (!isRingerAudible || Uri.EMPTY.equals(incomingCall.getRingtone())) { 174 if (isVibrationEnabled) { 175 mRingtone = factory.getHapticOnlyRingtone(); 176 if (mRingtone == null) { 177 completeHapticFuture(false /* ringtoneHasHaptics */); 178 return; 179 } 180 } else { 181 mRingtone = null; 182 completeHapticFuture(false /* ringtoneHasHaptics */); 183 return; 184 } 185 } 186 187 ThreadUtil.checkNotOnMainThread(); 188 Log.i(this, "handlePlay: Play ringtone."); 189 190 if (mRingtone == null) { 191 mRingtone = factory.getRingtone(incomingCall, volumeShaperConfig); 192 if (mRingtone == null) { 193 Uri ringtoneUri = incomingCall.getRingtone(); 194 String ringtoneUriString = (ringtoneUri == null) ? "null" : 195 ringtoneUri.toSafeString(); 196 Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " + 197 "factory. Skipping ringing. Uri was: " + ringtoneUriString); 198 completeHapticFuture(false /* ringtoneHasHaptics */); 199 return; 200 } 201 } 202 203 // With the ringtone to play now known, we can determine if it has haptic channels or 204 // not; we will complete the haptics future so the default vibration code in Ringer can 205 // know whether to trigger the vibrator. 206 if (mHapticsFuture != null && !mHapticsFuture.isDone()) { 207 boolean hasHaptics = factory.hasHapticChannels(mRingtone); 208 Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics, 209 isVibrationEnabled); 210 SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil(); 211 if (hasHaptics && (volumeShaperConfig == null 212 || systemSettingsUtil.enableAudioCoupledVibrationForRampingRinger())) { 213 AudioAttributes attributes = mRingtone.getAudioAttributes(); 214 Log.d(this, "handlePlay: %s haptic channel", 215 (isVibrationEnabled ? "unmuting" : "muting")); 216 mRingtone.setAudioAttributes( 217 new AudioAttributes.Builder(attributes) 218 .setHapticChannelsMuted(!isVibrationEnabled) 219 .build()); 220 } 221 completeHapticFuture(hasHaptics); 222 } 223 224 mRingtone.setLooping(true); 225 if (mRingtone.isPlaying()) { 226 Log.d(this, "Ringtone already playing."); 227 return; 228 } 229 mRingtone.play(); 230 Log.i(this, "Play ringtone, looping."); 231 } finally { 232 Log.cancelSubsession(session); 233 } 234 } 235 236 /** 237 * Stops the playback of the ringtone. Executes on the ringtone-thread. 238 */ 239 private void handleStop() { 240 ThreadUtil.checkNotOnMainThread(); 241 Log.i(this, "Stop ringtone."); 242 243 if (mRingtone != null) { 244 Log.d(this, "Ringtone.stop() invoked."); 245 mRingtone.stop(); 246 mRingtone = null; 247 } 248 249 synchronized(this) { 250 if (mHandler.hasMessages(EVENT_PLAY)) { 251 Log.v(this, "Keeping alive ringtone thread for subsequent play request."); 252 } else { 253 mHandler.removeMessages(EVENT_STOP); 254 mHandler.getLooper().quitSafely(); 255 mHandler = null; 256 Log.v(this, "Handler cleared."); 257 } 258 } 259 } 260 261 public boolean isPlaying() { 262 return mRingtone != null; 263 } 264 265 private void completeHapticFuture(boolean ringtoneHasHaptics) { 266 if (mHapticsFuture != null) { 267 mHapticsFuture.complete(ringtoneHasHaptics); 268 mHapticsFuture = null; 269 } 270 } 271 } 272