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