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