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