1 /*
2  * Copyright (C) 2013 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.testapps;
18 
19 import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
20 import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
21 
22 import android.content.BroadcastReceiver;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.media.AudioAttributes;
28 import android.media.AudioManager;
29 import android.media.MediaPlayer;
30 import android.media.ToneGenerator;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
35 import android.telecom.Conference;
36 import android.telecom.Connection;
37 import android.telecom.DisconnectCause;
38 import android.telecom.PhoneAccount;
39 import android.telecom.ConnectionRequest;
40 import android.telecom.ConnectionService;
41 import android.telecom.PhoneAccountHandle;
42 import android.telecom.TelecomManager;
43 import android.telecom.VideoProfile;
44 import android.telecom.Log;
45 import android.widget.Toast;
46 
47 import java.lang.String;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.Random;
51 
52 import static com.android.server.telecom.testapps.CallServiceNotifier.SIM_SUBSCRIPTION_ID2;
53 
54 /**
55  * Service which provides fake calls to test the ConnectionService interface.
56  */
57 public class TestConnectionService extends ConnectionService {
58     /**
59      * Intent extra used to pass along the video state for a new test call.
60      */
61     public static final String EXTRA_START_VIDEO_STATE = "extra_start_video_state";
62 
63     public static final String EXTRA_HANDLE = "extra_handle";
64 
65     /**
66      * If an outgoing call ends with 2879 (BUSY), the test CS will indicate the call is busy.
67      */
68     public static final String BUSY_SUFFIX = "2879";
69 
70     private static final String LOG_TAG = TestConnectionService.class.getSimpleName();
71 
72     private static TestConnectionService INSTANCE;
73 
74     /**
75      * Random number generator used to generate phone numbers.
76      */
77     private Random mRandom = new Random();
78 
79     private final class TestConference extends Conference {
80 
TestConference(Connection a, Connection b)81         public TestConference(Connection a, Connection b) {
82             super(null);
83             setConnectionCapabilities(
84                     Connection.CAPABILITY_SUPPORT_HOLD |
85                     Connection.CAPABILITY_HOLD |
86                     Connection.CAPABILITY_MUTE |
87                     Connection.CAPABILITY_MANAGE_CONFERENCE);
88             addConnection(a);
89             addConnection(b);
90 
91             a.setConference(this);
92             b.setConference(this);
93 
94             setActive();
95         }
96 
97         @Override
onDisconnect()98         public void onDisconnect() {
99             for (Connection c : getConnections()) {
100                 c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
101                 c.destroy();
102             }
103         }
104 
105         @Override
onSeparate(Connection connection)106         public void onSeparate(Connection connection) {
107             if (getConnections().contains(connection)) {
108                 connection.setConference(null);
109                 removeConnection(connection);
110             }
111         }
112 
113         @Override
onHold()114         public void onHold() {
115             for (Connection c : getConnections()) {
116                 c.setOnHold();
117             }
118             setOnHold();
119         }
120 
121         @Override
onUnhold()122         public void onUnhold() {
123             for (Connection c : getConnections()) {
124                 c.setActive();
125             }
126             setActive();
127         }
128     }
129 
130     final class TestConnection extends Connection {
131         private final boolean mIsIncoming;
132 
133         /** Used to cleanup camera and media when done with connection. */
134         private TestVideoProvider mTestVideoCallProvider;
135         private ConnectionRequest mOriginalRequest;
136         private RttChatbot mRttChatbot;
137 
138         private BroadcastReceiver mHangupReceiver = new BroadcastReceiver() {
139             @Override
140             public void onReceive(Context context, Intent intent) {
141                 setDisconnected(new DisconnectCause(DisconnectCause.MISSED));
142                 destroyCall(TestConnection.this);
143                 destroy();
144             }
145         };
146 
147         private BroadcastReceiver mUpgradeRequestReceiver = new BroadcastReceiver() {
148             @Override
149             public void onReceive(Context context, Intent intent) {
150                 final int request = Integer.parseInt(intent.getData().getSchemeSpecificPart());
151                 final VideoProfile videoProfile = new VideoProfile(request);
152                 mTestVideoCallProvider.receiveSessionModifyRequest(videoProfile);
153             }
154         };
155 
156         private BroadcastReceiver mRttUpgradeReceiver = new BroadcastReceiver() {
157             @Override
158             public void onReceive(Context context, Intent intent) {
159                 sendRemoteRttRequest();
160             }
161         };
162 
TestConnection(boolean isIncoming, ConnectionRequest request)163         TestConnection(boolean isIncoming, ConnectionRequest request) {
164             mIsIncoming = isIncoming;
165             mOriginalRequest = request;
166             // Assume all calls are video capable.
167             int capabilities = getConnectionCapabilities();
168             capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
169             capabilities |= CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL;
170             capabilities |= CAPABILITY_CAN_UPGRADE_TO_VIDEO;
171             capabilities |= CAPABILITY_MUTE;
172             capabilities |= CAPABILITY_SUPPORT_HOLD;
173             capabilities |= CAPABILITY_HOLD;
174             capabilities |= CAPABILITY_RESPOND_VIA_TEXT;
175             setConnectionCapabilities(capabilities);
176 
177             int properties = getConnectionProperties();
178             if (mOriginalRequest.isRequestingRtt()) {
179                 properties |= PROPERTY_IS_RTT;
180             }
181             setConnectionProperties(properties);
182 
183             if (isIncoming) {
184                 Bundle newExtras = (getExtras() == null) ? new Bundle() : getExtras();
185                 newExtras.putBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
186                 putExtras(newExtras);
187             }
188             LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
189                     mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS));
190             final IntentFilter filter =
191                     new IntentFilter(TestCallActivity.ACTION_SEND_UPGRADE_REQUEST);
192             filter.addDataScheme("int");
193             LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
194                     mUpgradeRequestReceiver, filter);
195 
196             LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
197                     mRttUpgradeReceiver,
198                     new IntentFilter(TestCallActivity.ACTION_REMOTE_RTT_UPGRADE));
199         }
200 
startOutgoing()201         void startOutgoing() {
202             setDialing();
203             mHandler.postDelayed(() -> {
204                 if (getAddress().getSchemeSpecificPart().endsWith(BUSY_SUFFIX)) {
205                     setDisconnected(new DisconnectCause(DisconnectCause.REMOTE, "Line busy",
206                             "Line busy", "Line busy", ToneGenerator.TONE_SUP_BUSY));
207                     destroyCall(this);
208                     destroy();
209                 } else {
210                     setActive();
211                     activateCall(TestConnection.this);
212                 }
213             }, 4000);
214             if (mOriginalRequest.isRequestingRtt()) {
215                 Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
216                 mRttChatbot = new RttChatbot(getApplicationContext(),
217                         mOriginalRequest.getRttTextStream());
218                 mRttChatbot.start();
219             }
220         }
221 
222         /** ${inheritDoc} */
223         @Override
onAbort()224         public void onAbort() {
225             destroyCall(this);
226             destroy();
227         }
228 
229         /** ${inheritDoc} */
230         @Override
onAnswer(int videoState)231         public void onAnswer(int videoState) {
232             setVideoState(videoState);
233             activateCall(this);
234             setActive();
235             updateConferenceable();
236             if (mOriginalRequest.isRequestingRtt()) {
237                 Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
238                 mRttChatbot = new RttChatbot(getApplicationContext(),
239                         mOriginalRequest.getRttTextStream());
240                 mRttChatbot.start();
241             }
242         }
243 
244         /** ${inheritDoc} */
245         @Override
onPlayDtmfTone(char c)246         public void onPlayDtmfTone(char c) {
247             if (c == '1') {
248                 setDialing();
249             }
250         }
251 
252         /** ${inheritDoc} */
253         @Override
onStopDtmfTone()254         public void onStopDtmfTone() { }
255 
256         /** ${inheritDoc} */
257         @Override
onDisconnect()258         public void onDisconnect() {
259             setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
260             destroyCall(this);
261             destroy();
262         }
263 
264         /** ${inheritDoc} */
265         @Override
onHold()266         public void onHold() {
267             setOnHold();
268         }
269 
270         /** ${inheritDoc} */
271         @Override
onReject()272         public void onReject() {
273             setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
274             destroyCall(this);
275             destroy();
276         }
277 
278         /** ${inheritDoc} */
279         @Override
onUnhold()280         public void onUnhold() {
281             setActive();
282         }
283 
284         @Override
onStopRtt()285         public void onStopRtt() {
286             int newProperties = getConnectionProperties() & ~PROPERTY_IS_RTT;
287             setConnectionProperties(newProperties);
288             mRttChatbot.stop();
289             mRttChatbot = null;
290         }
291 
292         @Override
handleRttUpgradeResponse(RttTextStream rttTextStream)293         public void handleRttUpgradeResponse(RttTextStream rttTextStream) {
294             Log.i(this, "RTT request response was %s", rttTextStream == null);
295             if (rttTextStream != null) {
296                 mRttChatbot = new RttChatbot(getApplicationContext(), rttTextStream);
297                 mRttChatbot.start();
298                 sendRttInitiationSuccess();
299             }
300         }
301 
302         @Override
onStartRtt(RttTextStream textStream)303         public void onStartRtt(RttTextStream textStream) {
304             boolean doAccept = Math.random() < 0.5;
305             if (doAccept) {
306                 Log.i(this, "Accepting RTT request.");
307                 mRttChatbot = new RttChatbot(getApplicationContext(), textStream);
308                 mRttChatbot.start();
309                 sendRttInitiationSuccess();
310             } else {
311                 sendRttInitiationFailure(RttModifyStatus.SESSION_MODIFY_REQUEST_FAIL);
312             }
313         }
314 
315         public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
316             mTestVideoCallProvider = testVideoCallProvider;
317         }
318 
319         public void cleanup() {
320             LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
321                     mHangupReceiver);
322             LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
323                     mUpgradeRequestReceiver);
324         }
325 
326         /**
327          * Stops playback of test videos.
328          */
329         private void stopAndCleanupMedia() {
330             if (mTestVideoCallProvider != null) {
331                 mTestVideoCallProvider.stopAndCleanupMedia();
332                 mTestVideoCallProvider.stopCamera();
333             }
334         }
335     }
336 
337     private final List<TestConnection> mCalls = new ArrayList<>();
338     private final Handler mHandler = new Handler();
339 
340     /** Used to play an audio tone during a call. */
341     private MediaPlayer mMediaPlayer;
342 
343     @Override
344     public void onCreate() {
345         INSTANCE = this;
346     }
347 
348     @Override
349     public boolean onUnbind(Intent intent) {
350         log("onUnbind");
351         mMediaPlayer = null;
352         return super.onUnbind(intent);
353     }
354 
355     @Override
356     public void onConference(Connection a, Connection b) {
357         addConference(new TestConference(a, b));
358     }
359 
360     @Override
361     public Connection onCreateOutgoingConnection(
362             PhoneAccountHandle connectionManagerAccount,
363             final ConnectionRequest originalRequest) {
364 
365         final Uri handle = originalRequest.getAddress();
366         String number = originalRequest.getAddress().getSchemeSpecificPart();
367         log("call, number: " + number);
368 
369         // Crash on 555-DEAD to test call service crashing.
370         if ("5550340".equals(number)) {
371             throw new RuntimeException("Goodbye, cruel world.");
372         }
373 
374         Bundle extras = originalRequest.getExtras();
375         String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE);
376         Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS);
377 
378         if (extras.containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) {
379             String callSubject = extras.getString(TelecomManager.EXTRA_CALL_SUBJECT);
380             log("Got subject: " + callSubject);
381             Toast.makeText(getApplicationContext(), "Got subject :" + callSubject,
382                     Toast.LENGTH_SHORT).show();
383         }
384 
385         log("gateway package [" + gatewayPackage + "], original handle [" +
386                 originalHandle + "]");
387 
388         final TestConnection connection =
389                 new TestConnection(false /* isIncoming */, originalRequest);
390         setAddress(connection, handle);
391 
392         // If the number starts with 555, then we handle it ourselves. If not, then we
393         // use a remote connection service.
394         // TODO: Have a special phone number to test the account-picker dialog flow.
395         if (number != null && number.startsWith("555")) {
396             // Normally we would use the original request as is, but for testing purposes, we are
397             // adding ".." to the end of the number to follow its path more easily through the logs.
398             final ConnectionRequest request = new ConnectionRequest(
399                     originalRequest.getAccountHandle(),
400                     Uri.fromParts(handle.getScheme(),
401                     handle.getSchemeSpecificPart() + "..", ""),
402                     originalRequest.getExtras(),
403                     originalRequest.getVideoState());
404             connection.setVideoState(originalRequest.getVideoState());
405             addVideoProvider(connection);
406             addCall(connection);
407             connection.startOutgoing();
408 
409             for (Connection c : getAllConnections()) {
410                 c.setOnHold();
411             }
412         } else {
413             log("Not a test number");
414         }
415         return connection;
416     }
417 
418     @Override
419     public Connection onCreateIncomingConnection(
420             PhoneAccountHandle connectionManagerAccount,
421             final ConnectionRequest request) {
422         PhoneAccountHandle accountHandle = request.getAccountHandle();
423         ComponentName componentName = new ComponentName(this, TestConnectionService.class);
424 
425         if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
426             final TestConnection connection = new TestConnection(true, request);
427             // Get the stashed intent extra that determines if this is a video call or audio call.
428             Bundle extras = request.getExtras();
429             int videoState = extras.getInt(EXTRA_START_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
430             Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
431 
432             // Use test number for testing incoming calls.
433             Uri address = providedHandle == null ?
434                     Uri.fromParts(PhoneAccount.SCHEME_TEL, getRandomNumber(
435                             VideoProfile.isVideo(videoState)), null)
436                     : providedHandle;
437             connection.setVideoState(videoState);
438 
439             Bundle connectionExtras = connection.getExtras();
440             if (connectionExtras == null) {
441                 connectionExtras = new Bundle();
442             }
443 
444             // Randomly choose a varying length call subject.
445             int subjectFormat = mRandom.nextInt(3);
446             if (subjectFormat == 0) {
447                 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT,
448                         "This is a test of call subject lines. Subjects for a call can be long " +
449                                 " and can go even longer.");
450             } else if (subjectFormat == 1) {
451                 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT,
452                         "This is a test of call subject lines.");
453             }
454 
455             connection.putExtras(connectionExtras);
456 
457             setAddress(connection, address);
458 
459             addVideoProvider(connection);
460 
461             addCall(connection);
462 
463             connection.setVideoState(videoState);
464             return connection;
465         } else {
466             return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
467                     "Invalid inputs: " + accountHandle + " " + componentName));
468         }
469     }
470 
471     @Override
472     public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
473             final ConnectionRequest request) {
474         PhoneAccountHandle accountHandle = request.getAccountHandle();
475         ComponentName componentName = new ComponentName(this, TestConnectionService.class);
476         if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
477             final TestConnection connection = new TestConnection(false, request);
478             final Bundle extras = request.getExtras();
479             final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
480 
481             Uri handle = providedHandle == null ?
482                     Uri.fromParts(PhoneAccount.SCHEME_TEL, getRandomNumber(false), null)
483                     : providedHandle;
484 
485             connection.setAddress(handle,  TelecomManager.PRESENTATION_ALLOWED);
486             connection.setDialing();
487 
488             addCall(connection);
489             return connection;
490         } else {
491             return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
492                     "Invalid inputs: " + accountHandle + " " + componentName));
493         }
494     }
495 
496     public static TestConnectionService getInstance() {
497         return INSTANCE;
498     }
499 
500     public void switchPhoneAccount() {
501         if (!mCalls.isEmpty()) {
502             TestConnection c = mCalls.get(0);
503             c.notifyPhoneAccountChanged(CallServiceNotifier.getInstance()
504                     .getPhoneAccountHandle(SIM_SUBSCRIPTION_ID2));
505         } else {
506             Log.i(this, "Couldn't switch PhoneAccount, call is null!");
507         }
508     }
509     public void switchPhoneAccountWrong() {
510         PhoneAccountHandle pah = new PhoneAccountHandle(
511                 new ComponentName("com.android.phone",
512                 "com.android.services.telephony.TelephonyConnectionService"), "TEST");
513         if (!mCalls.isEmpty()) {
514             TestConnection c = mCalls.get(0);
515             try {
516                 c.notifyPhoneAccountChanged(pah);
517             } catch (SecurityException e) {
518                 Toast.makeText(getApplicationContext(), "SwitchPhoneAccount: Pass",
519                         Toast.LENGTH_SHORT).show();
520             }
521         } else {
522             Log.i(this, "Couldn't switch PhoneAccount, call is null!");
523         }
524     }
525 
526     private void addVideoProvider(TestConnection connection) {
527         TestVideoProvider testVideoCallProvider =
528                 new TestVideoProvider(getApplicationContext(), connection);
529         connection.setVideoProvider(testVideoCallProvider);
530 
531         // Keep reference to original so we can clean up the media players later.
532         connection.setTestVideoCallProvider(testVideoCallProvider);
533     }
534 
535     private void activateCall(TestConnection connection) {
536         if (mMediaPlayer == null) {
537             mMediaPlayer = createMediaPlayer();
538         }
539         if (!mMediaPlayer.isPlaying()) {
540             mMediaPlayer.start();
541         }
542     }
543 
544     private void destroyCall(TestConnection connection) {
545         connection.cleanup();
546         mCalls.remove(connection);
547 
548         // Ensure any playing media and camera resources are released.
549         connection.stopAndCleanupMedia();
550 
551         // Stops audio if there are no more calls.
552         if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
553             mMediaPlayer.stop();
554             mMediaPlayer.release();
555             mMediaPlayer = createMediaPlayer();
556         }
557 
558         updateConferenceable();
559     }
560 
561     private void addCall(TestConnection connection) {
562         mCalls.add(connection);
563         updateConferenceable();
564     }
565 
566     private void updateConferenceable() {
567         List<Connection> freeConnections = new ArrayList<>();
568         freeConnections.addAll(mCalls);
569         for (int i = 0; i < freeConnections.size(); i++) {
570             if (freeConnections.get(i).getConference() != null) {
571                 freeConnections.remove(i);
572             }
573         }
574         for (int i = 0; i < freeConnections.size(); i++) {
575             Connection c = freeConnections.remove(i);
576             c.setConferenceableConnections(freeConnections);
577             freeConnections.add(i, c);
578         }
579     }
580 
581     private void setAddress(Connection connection, Uri address) {
582         connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
583         if ("5551234".equals(address.getSchemeSpecificPart())) {
584             connection.setCallerDisplayName("Hello World", TelecomManager.PRESENTATION_ALLOWED);
585         }
586     }
587 
588     private MediaPlayer createMediaPlayer() {
589         AudioAttributes attributes = new AudioAttributes.Builder()
590                 .setUsage(USAGE_VOICE_COMMUNICATION)
591                 .setContentType(CONTENT_TYPE_SPEECH)
592                 .build();
593 
594         final int audioSessionId = ((AudioManager) getSystemService(
595                 Context.AUDIO_SERVICE)).generateAudioSessionId();
596         // Prepare the media player to play a tone when there is a call.
597         MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop, attributes,
598                 audioSessionId);
599         mediaPlayer.setLooping(true);
600         return mediaPlayer;
601     }
602 
603     private static void log(String msg) {
604         Log.w("telecomtestcs", "[TestConnectionService] " + msg);
605     }
606 
607     /**
608      * Generates a random phone number of format 555YXXX.  Where Y will be {@code 1} if the
609      * phone number is for a video call and {@code 0} for an audio call.  XXX is a randomly
610      * generated phone number.
611      *
612      * @param isVideo {@code True} if the call is a video call.
613      * @return The phone number.
614      */
615     private String getRandomNumber(boolean isVideo) {
616         int videoDigit = isVideo ? 1 : 0;
617         int number = mRandom.nextInt(999);
618         return String.format("555%s%03d", videoDigit, number);
619     }
620 }
621 
622