/* * Copyright 2014, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.telecom; import android.annotation.NonNull; import android.annotation.Nullable; import android.media.AudioAttributes; import android.media.Ringtone; import android.media.VolumeShaper; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.telecom.Log; import android.telecom.Logging.Session; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.util.Preconditions; import java.util.concurrent.CompletableFuture; /** * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be * used from the main thread. */ @VisibleForTesting public class AsyncRingtonePlayer { // Message codes used with the ringtone thread. private static final int EVENT_PLAY = 1; private static final int EVENT_STOP = 2; /** Handler running on the ringtone thread. */ private Handler mHandler; /** The current ringtone. Only used by the ringtone thread. */ private Ringtone mRingtone; /** * CompletableFuture which signals a caller when we know whether a ringtone will play haptics * or not. */ private CompletableFuture mHapticsFuture = null; public AsyncRingtonePlayer() { // Empty } /** * Plays the appropriate ringtone for the specified call. * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change * the volume of the ringtone as it plays. * * @param factory The {@link RingtoneFactory}. * @param incomingCall The ringing {@link Call}. * @param volumeShaperConfig An optional {@link VolumeShaper.Configuration} which is applied to * the ringtone to change its volume while it rings. * @param isVibrationEnabled {@code true} if the settings and DND configuration of the device * is such that the vibrator should be used, {@code false} otherwise. * @return A {@link CompletableFuture} which on completion indicates whether or not the ringtone * has a haptic track. {@code True} indicates that a haptic track is present on the * ringtone; in this case the default vibration in {@link Ringer} should not be played. * {@code False} indicates that a haptic track is NOT present on the ringtone; * in this case the default vibration in {@link Ringer} should be trigger if needed. */ public @NonNull CompletableFuture play(RingtoneFactory factory, Call incomingCall, @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean isVibrationEnabled) { Log.d(this, "Posting play."); if (mHapticsFuture == null) { mHapticsFuture = new CompletableFuture<>(); } SomeArgs args = SomeArgs.obtain(); args.arg1 = factory; args.arg2 = incomingCall; args.arg3 = volumeShaperConfig; args.arg4 = isVibrationEnabled; args.arg5 = Log.createSubsession(); postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args); return mHapticsFuture; } /** Stops playing the ringtone. */ public void stop() { Log.d(this, "Posting stop."); postMessage(EVENT_STOP, false /* shouldCreateHandler */, null); } /** * Posts a message to the ringtone-thread handler. Creates the handler if specified by the * parameter shouldCreateHandler. * * @param messageCode The message to post. * @param shouldCreateHandler True when a handler should be created to handle this message. */ private void postMessage(int messageCode, boolean shouldCreateHandler, SomeArgs args) { synchronized(this) { if (mHandler == null && shouldCreateHandler) { mHandler = getNewHandler(); } if (mHandler == null) { Log.d(this, "Message %d skipped because there is no handler.", messageCode); } else { mHandler.obtainMessage(messageCode, args).sendToTarget(); } } } /** * Creates a new ringtone Handler running in its own thread. */ private Handler getNewHandler() { Preconditions.checkState(mHandler == null); HandlerThread thread = new HandlerThread("ringtone-player"); thread.start(); return new Handler(thread.getLooper(), null /*callback*/, true /*async*/) { @Override public void handleMessage(Message msg) { switch(msg.what) { case EVENT_PLAY: handlePlay((SomeArgs) msg.obj); break; case EVENT_STOP: handleStop(); break; } } }; } /** * Starts the actual playback of the ringtone. Executes on ringtone-thread. */ private void handlePlay(SomeArgs args) { RingtoneFactory factory = (RingtoneFactory) args.arg1; Call incomingCall = (Call) args.arg2; VolumeShaper.Configuration volumeShaperConfig = (VolumeShaper.Configuration) args.arg3; boolean isVibrationEnabled = (boolean) args.arg4; Session session = (Session) args.arg5; args.recycle(); Log.continueSession(session, "ARP.hP"); try { // don't bother with any of this if there is an EVENT_STOP waiting. if (mHandler.hasMessages(EVENT_STOP)) { if (mHapticsFuture != null) { mHapticsFuture.complete(false /* ringtoneHasHaptics */); mHapticsFuture = null; } return; } // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. Do not play // anything. if (Uri.EMPTY.equals(incomingCall.getRingtone())) { mRingtone = null; if (mHapticsFuture != null) { mHapticsFuture.complete(false /* ringtoneHasHaptics */); mHapticsFuture = null; } return; } ThreadUtil.checkNotOnMainThread(); Log.i(this, "handlePlay: Play ringtone."); if (mRingtone == null) { mRingtone = factory.getRingtone(incomingCall, volumeShaperConfig); if (mRingtone == null) { Uri ringtoneUri = incomingCall.getRingtone(); String ringtoneUriString = (ringtoneUri == null) ? "null" : ringtoneUri.toSafeString(); Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " + "factory. Skipping ringing. Uri was: " + ringtoneUriString); if (mHapticsFuture != null) { mHapticsFuture.complete(false /* ringtoneHasHaptics */); mHapticsFuture = null; } return; } // With the ringtone to play now known, we can determine if it has haptic channels or // not; we will complete the haptics future so the default vibration code in Ringer // can know whether to trigger the vibrator. if (mHapticsFuture != null && !mHapticsFuture.isDone()) { boolean hasHaptics = factory.hasHapticChannels(mRingtone); Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics, isVibrationEnabled); SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil(); if (hasHaptics && (volumeShaperConfig == null || systemSettingsUtil.enableAudioCoupledVibrationForRampingRinger())) { AudioAttributes attributes = mRingtone.getAudioAttributes(); Log.d(this, "handlePlay: %s haptic channel", (isVibrationEnabled ? "unmuting" : "muting")); mRingtone.setAudioAttributes( new AudioAttributes.Builder(attributes) .setHapticChannelsMuted(!isVibrationEnabled) .build()); } mHapticsFuture.complete(hasHaptics); mHapticsFuture = null; } } mRingtone.setLooping(true); if (mRingtone.isPlaying()) { Log.d(this, "Ringtone already playing."); return; } mRingtone.play(); Log.i(this, "Play ringtone, looping."); } finally { Log.cancelSubsession(session); } } /** * Stops the playback of the ringtone. Executes on the ringtone-thread. */ private void handleStop() { ThreadUtil.checkNotOnMainThread(); Log.i(this, "Stop ringtone."); if (mRingtone != null) { Log.d(this, "Ringtone.stop() invoked."); mRingtone.stop(); mRingtone = null; } synchronized(this) { if (mHandler.hasMessages(EVENT_PLAY)) { Log.v(this, "Keeping alive ringtone thread for subsequent play request."); } else { mHandler.removeMessages(EVENT_STOP); mHandler.getLooper().quitSafely(); mHandler = null; Log.v(this, "Handler cleared."); } } } }