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