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 AsyncRingtonePlayer()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 */ play(RingtoneFactory factory, Call incomingCall, @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean isVibrationEnabled)80 public @NonNull CompletableFuture<Boolean> play(RingtoneFactory factory, Call incomingCall, 81 @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean isVibrationEnabled) { 82 Log.d(this, "Posting play."); 83 if (mHapticsFuture == null) { 84 mHapticsFuture = new CompletableFuture<>(); 85 } 86 SomeArgs args = SomeArgs.obtain(); 87 args.arg1 = factory; 88 args.arg2 = incomingCall; 89 args.arg3 = volumeShaperConfig; 90 args.arg4 = isVibrationEnabled; 91 args.arg5 = Log.createSubsession(); 92 postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args); 93 return mHapticsFuture; 94 } 95 96 /** Stops playing the ringtone. */ stop()97 public void stop() { 98 Log.d(this, "Posting stop."); 99 postMessage(EVENT_STOP, false /* shouldCreateHandler */, null); 100 } 101 102 /** 103 * Posts a message to the ringtone-thread handler. Creates the handler if specified by the 104 * parameter shouldCreateHandler. 105 * 106 * @param messageCode The message to post. 107 * @param shouldCreateHandler True when a handler should be created to handle this message. 108 */ postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args)109 private void postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args) { 110 synchronized(this) { 111 if (mHandler == null && shouldCreateHandler) { 112 mHandler = getNewHandler(); 113 } 114 115 if (mHandler == null) { 116 Log.d(this, "Message %d skipped because there is no handler.", messageCode); 117 } else { 118 mHandler.obtainMessage(messageCode, args).sendToTarget(); 119 } 120 } 121 } 122 123 /** 124 * Creates a new ringtone Handler running in its own thread. 125 */ getNewHandler()126 private Handler getNewHandler() { 127 Preconditions.checkState(mHandler == null); 128 129 HandlerThread thread = new HandlerThread("ringtone-player"); 130 thread.start(); 131 132 return new Handler(thread.getLooper(), null /*callback*/, true /*async*/) { 133 @Override 134 public void handleMessage(Message msg) { 135 switch(msg.what) { 136 case EVENT_PLAY: 137 handlePlay((SomeArgs) msg.obj); 138 break; 139 case EVENT_STOP: 140 handleStop(); 141 break; 142 } 143 } 144 }; 145 } 146 147 /** 148 * Starts the actual playback of the ringtone. Executes on ringtone-thread. 149 */ 150 private void handlePlay(SomeArgs args) { 151 RingtoneFactory factory = (RingtoneFactory) args.arg1; 152 Call incomingCall = (Call) args.arg2; 153 VolumeShaper.Configuration volumeShaperConfig = (VolumeShaper.Configuration) args.arg3; 154 boolean isVibrationEnabled = (boolean) args.arg4; 155 Session session = (Session) args.arg5; 156 args.recycle(); 157 158 Log.continueSession(session, "ARP.hP"); 159 try { 160 // don't bother with any of this if there is an EVENT_STOP waiting. 161 if (mHandler.hasMessages(EVENT_STOP)) { 162 if (mHapticsFuture != null) { 163 mHapticsFuture.complete(false /* ringtoneHasHaptics */); 164 mHapticsFuture = null; 165 } 166 return; 167 } 168 169 // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. Do not play 170 // anything. 171 if (Uri.EMPTY.equals(incomingCall.getRingtone())) { 172 mRingtone = null; 173 if (mHapticsFuture != null) { 174 mHapticsFuture.complete(false /* ringtoneHasHaptics */); 175 mHapticsFuture = null; 176 } 177 178 return; 179 } 180 181 ThreadUtil.checkNotOnMainThread(); 182 Log.i(this, "handlePlay: Play ringtone."); 183 184 if (mRingtone == null) { 185 mRingtone = factory.getRingtone(incomingCall, volumeShaperConfig); 186 if (mRingtone == null) { 187 Uri ringtoneUri = incomingCall.getRingtone(); 188 String ringtoneUriString = (ringtoneUri == null) ? "null" : 189 ringtoneUri.toSafeString(); 190 Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " + 191 "factory. Skipping ringing. Uri was: " + ringtoneUriString); 192 if (mHapticsFuture != null) { 193 mHapticsFuture.complete(false /* ringtoneHasHaptics */); 194 mHapticsFuture = null; 195 } 196 return; 197 } 198 199 // With the ringtone to play now known, we can determine if it has haptic channels or 200 // not; we will complete the haptics future so the default vibration code in Ringer 201 // can know whether to trigger the vibrator. 202 if (mHapticsFuture != null && !mHapticsFuture.isDone()) { 203 boolean hasHaptics = factory.hasHapticChannels(mRingtone); 204 Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics, 205 isVibrationEnabled); 206 SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil(); 207 if (hasHaptics && (volumeShaperConfig == null 208 || systemSettingsUtil.enableAudioCoupledVibrationForRampingRinger())) { 209 AudioAttributes attributes = mRingtone.getAudioAttributes(); 210 Log.d(this, "handlePlay: %s haptic channel", 211 (isVibrationEnabled ? "unmuting" : "muting")); 212 mRingtone.setAudioAttributes( 213 new AudioAttributes.Builder(attributes) 214 .setHapticChannelsMuted(!isVibrationEnabled) 215 .build()); 216 } 217 mHapticsFuture.complete(hasHaptics); 218 mHapticsFuture = null; 219 } 220 } 221 222 mRingtone.setLooping(true); 223 if (mRingtone.isPlaying()) { 224 Log.d(this, "Ringtone already playing."); 225 return; 226 } 227 mRingtone.play(); 228 Log.i(this, "Play ringtone, looping."); 229 } finally { 230 Log.cancelSubsession(session); 231 } 232 } 233 234 /** 235 * Stops the playback of the ringtone. Executes on the ringtone-thread. 236 */ 237 private void handleStop() { 238 ThreadUtil.checkNotOnMainThread(); 239 Log.i(this, "Stop ringtone."); 240 241 if (mRingtone != null) { 242 Log.d(this, "Ringtone.stop() invoked."); 243 mRingtone.stop(); 244 mRingtone = null; 245 } 246 247 synchronized(this) { 248 if (mHandler.hasMessages(EVENT_PLAY)) { 249 Log.v(this, "Keeping alive ringtone thread for subsequent play request."); 250 } else { 251 mHandler.removeMessages(EVENT_STOP); 252 mHandler.getLooper().quitSafely(); 253 mHandler = null; 254 Log.v(this, "Handler cleared."); 255 } 256 } 257 } 258 } 259