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