TestConnectionService.java revision 95ac44451d432cbbb3908efed490e90a803edf22
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
17package com.android.server.telecom.testapps;
18
19import android.content.BroadcastReceiver;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.media.MediaPlayer;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Handler;
28import android.support.v4.content.LocalBroadcastManager;
29import android.telecom.AudioState;
30import android.telecom.Conference;
31import android.telecom.Connection;
32import android.telecom.DisconnectCause;
33import android.telecom.PhoneAccount;
34import android.telecom.ConnectionRequest;
35import android.telecom.ConnectionService;
36import android.telecom.PhoneAccountHandle;
37import android.telecom.TelecomManager;
38import android.telecom.VideoProfile;
39import android.util.Log;
40
41import com.android.server.telecom.testapps.R;
42
43import java.lang.String;
44import java.util.ArrayList;
45import java.util.List;
46import java.util.Random;
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 */
52public class TestConnectionService extends ConnectionService {
53    /**
54     * Intent extra used to pass along whether a call is video or audio based on the user's choice
55     * in the notification.
56     */
57    public static final String EXTRA_IS_VIDEO_CALL = "extra_is_video_call";
58
59    public static final String EXTRA_HANDLE = "extra_handle";
60
61    /**
62     * Random number generator used to generate phone numbers.
63     */
64    private Random mRandom = new Random();
65
66    private final class TestConference extends Conference {
67
68        private final Connection.Listener mConnectionListener = new Connection.Listener() {
69            @Override
70            public void onDestroyed(Connection c) {
71                removeConnection(c);
72                if (getConnections().size() == 0) {
73                    setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
74                    destroy();
75                }
76            }
77        };
78
79        public TestConference(Connection a, Connection b) {
80            super(null);
81            setConnectionCapabilities(
82                    Connection.CAPABILITY_SUPPORT_HOLD |
83                    Connection.CAPABILITY_HOLD |
84                    Connection.CAPABILITY_MUTE |
85                    Connection.CAPABILITY_MANAGE_CONFERENCE);
86            addConnection(a);
87            addConnection(b);
88
89            a.addConnectionListener(mConnectionListener);
90            b.addConnectionListener(mConnectionListener);
91
92            a.setConference(this);
93            b.setConference(this);
94
95            setActive();
96        }
97
98        @Override
99        public void onDisconnect() {
100            for (Connection c : getConnections()) {
101                c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
102                c.destroy();
103            }
104        }
105
106        @Override
107        public void onSeparate(Connection connection) {
108            if (getConnections().contains(connection)) {
109                connection.setConference(null);
110                removeConnection(connection);
111                connection.removeConnectionListener(mConnectionListener);
112            }
113        }
114
115        @Override
116        public void onHold() {
117            for (Connection c : getConnections()) {
118                c.setOnHold();
119            }
120            setOnHold();
121        }
122
123        @Override
124        public void onUnhold() {
125            for (Connection c : getConnections()) {
126                c.setActive();
127            }
128            setActive();
129        }
130    }
131
132    final class TestConnection extends Connection {
133        private final boolean mIsIncoming;
134
135        /** Used to cleanup camera and media when done with connection. */
136        private TestVideoProvider mTestVideoCallProvider;
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        TestConnection(boolean isIncoming) {
157            mIsIncoming = isIncoming;
158            // Assume all calls are video capable.
159            int capabilities = getConnectionCapabilities();
160            capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
161            capabilities |= CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL;
162            capabilities |= CAPABILITY_CAN_UPGRADE_TO_VIDEO;
163            capabilities |= CAPABILITY_MUTE;
164            capabilities |= CAPABILITY_SUPPORT_HOLD;
165            capabilities |= CAPABILITY_HOLD;
166            setConnectionCapabilities(capabilities);
167
168            LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
169                    mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS));
170            final IntentFilter filter =
171                    new IntentFilter(TestCallActivity.ACTION_SEND_UPGRADE_REQUEST);
172            filter.addDataScheme("int");
173            LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
174                    mUpgradeRequestReceiver, filter);
175        }
176
177        void startOutgoing() {
178            setDialing();
179            mHandler.postDelayed(new Runnable() {
180                @Override
181                public void run() {
182                    setActive();
183                    activateCall(TestConnection.this);
184                }
185            }, 4000);
186        }
187
188        /** ${inheritDoc} */
189        @Override
190        public void onAbort() {
191            destroyCall(this);
192            destroy();
193        }
194
195        /** ${inheritDoc} */
196        @Override
197        public void onAnswer(int videoState) {
198            setVideoState(videoState);
199            activateCall(this);
200            setActive();
201            updateConferenceable();
202        }
203
204        /** ${inheritDoc} */
205        @Override
206        public void onPlayDtmfTone(char c) {
207            if (c == '1') {
208                setDialing();
209            }
210        }
211
212        /** ${inheritDoc} */
213        @Override
214        public void onStopDtmfTone() { }
215
216        /** ${inheritDoc} */
217        @Override
218        public void onDisconnect() {
219            setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
220            destroyCall(this);
221            destroy();
222        }
223
224        /** ${inheritDoc} */
225        @Override
226        public void onHold() {
227            setOnHold();
228        }
229
230        /** ${inheritDoc} */
231        @Override
232        public void onReject() {
233            setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
234            destroyCall(this);
235            destroy();
236        }
237
238        /** ${inheritDoc} */
239        @Override
240        public void onUnhold() {
241            setActive();
242        }
243
244        @Override
245        public void onAudioStateChanged(AudioState state) { }
246
247
248        public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
249            mTestVideoCallProvider = testVideoCallProvider;
250        }
251
252        public void cleanup() {
253            LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
254                    mHangupReceiver);
255            LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
256                    mUpgradeRequestReceiver);
257        }
258
259        /**
260         * Stops playback of test videos.
261         */
262        private void stopAndCleanupMedia() {
263            if (mTestVideoCallProvider != null) {
264                mTestVideoCallProvider.stopAndCleanupMedia();
265                mTestVideoCallProvider.stopCamera();
266            }
267        }
268    }
269
270    private final List<TestConnection> mCalls = new ArrayList<>();
271    private final Handler mHandler = new Handler();
272
273    /** Used to play an audio tone during a call. */
274    private MediaPlayer mMediaPlayer;
275
276    @Override
277    public boolean onUnbind(Intent intent) {
278        log("onUnbind");
279        mMediaPlayer = null;
280        return super.onUnbind(intent);
281    }
282
283    @Override
284    public void onConference(Connection a, Connection b) {
285        addConference(new TestConference(a, b));
286    }
287
288    @Override
289    public Connection onCreateOutgoingConnection(
290            PhoneAccountHandle connectionManagerAccount,
291            final ConnectionRequest originalRequest) {
292
293        final Uri handle = originalRequest.getAddress();
294        String number = originalRequest.getAddress().getSchemeSpecificPart();
295        log("call, number: " + number);
296
297        // Crash on 555-DEAD to test call service crashing.
298        if ("5550340".equals(number)) {
299            throw new RuntimeException("Goodbye, cruel world.");
300        }
301
302        Bundle extras = originalRequest.getExtras();
303        String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE);
304        Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS);
305
306        log("gateway package [" + gatewayPackage + "], original handle [" +
307                originalHandle + "]");
308
309        final TestConnection connection = new TestConnection(false /* isIncoming */);
310        connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED);
311
312        // If the number starts with 555, then we handle it ourselves. If not, then we
313        // use a remote connection service.
314        // TODO: Have a special phone number to test the account-picker dialog flow.
315        if (number != null && number.startsWith("555")) {
316            // Normally we would use the original request as is, but for testing purposes, we are
317            // adding ".." to the end of the number to follow its path more easily through the logs.
318            final ConnectionRequest request = new ConnectionRequest(
319                    originalRequest.getAccountHandle(),
320                    Uri.fromParts(handle.getScheme(),
321                    handle.getSchemeSpecificPart() + "..", ""),
322                    originalRequest.getExtras(),
323                    originalRequest.getVideoState());
324            connection.setVideoState(originalRequest.getVideoState());
325            addVideoProvider(connection);
326            addCall(connection);
327            connection.startOutgoing();
328
329            for (Connection c : getAllConnections()) {
330                c.setOnHold();
331            }
332        } else {
333            log("Not a test number");
334        }
335        return connection;
336    }
337
338    @Override
339    public Connection onCreateIncomingConnection(
340            PhoneAccountHandle connectionManagerAccount,
341            final ConnectionRequest request) {
342        PhoneAccountHandle accountHandle = request.getAccountHandle();
343        ComponentName componentName = new ComponentName(this, TestConnectionService.class);
344
345        if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
346            final TestConnection connection = new TestConnection(true);
347            // Get the stashed intent extra that determines if this is a video call or audio call.
348            Bundle extras = request.getExtras();
349            boolean isVideoCall = extras.getBoolean(EXTRA_IS_VIDEO_CALL);
350            Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
351
352            // Use dummy number for testing incoming calls.
353            Uri address = providedHandle == null ?
354                    Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(isVideoCall), null)
355                    : providedHandle;
356
357            int videoState = isVideoCall ?
358                    VideoProfile.VideoState.BIDIRECTIONAL :
359                    VideoProfile.VideoState.AUDIO_ONLY;
360            connection.setVideoState(videoState);
361            connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
362
363            addVideoProvider(connection);
364
365            addCall(connection);
366
367            ConnectionRequest newRequest = new ConnectionRequest(
368                    request.getAccountHandle(),
369                    address,
370                    request.getExtras(),
371                    videoState);
372            connection.setVideoState(videoState);
373            return connection;
374        } else {
375            return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
376                    "Invalid inputs: " + accountHandle + " " + componentName));
377        }
378    }
379
380    @Override
381    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
382            final ConnectionRequest request) {
383        PhoneAccountHandle accountHandle = request.getAccountHandle();
384        ComponentName componentName = new ComponentName(this, TestConnectionService.class);
385        if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
386            final TestConnection connection = new TestConnection(false);
387            final Bundle extras = request.getExtras();
388            final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
389
390            Uri handle = providedHandle == null ?
391                    Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null)
392                    : providedHandle;
393
394            connection.setAddress(handle,  TelecomManager.PRESENTATION_ALLOWED);
395            connection.setDialing();
396
397            addCall(connection);
398            return connection;
399        } else {
400            return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
401                    "Invalid inputs: " + accountHandle + " " + componentName));
402        }
403    }
404
405    private void addVideoProvider(TestConnection connection) {
406        TestVideoProvider testVideoCallProvider =
407                new TestVideoProvider(getApplicationContext(), connection);
408        connection.setVideoProvider(testVideoCallProvider);
409
410        // Keep reference to original so we can clean up the media players later.
411        connection.setTestVideoCallProvider(testVideoCallProvider);
412    }
413
414    private void activateCall(TestConnection connection) {
415        if (mMediaPlayer == null) {
416            mMediaPlayer = createMediaPlayer();
417        }
418        if (!mMediaPlayer.isPlaying()) {
419            mMediaPlayer.start();
420        }
421    }
422
423    private void destroyCall(TestConnection connection) {
424        connection.cleanup();
425        mCalls.remove(connection);
426
427        // Ensure any playing media and camera resources are released.
428        connection.stopAndCleanupMedia();
429
430        // Stops audio if there are no more calls.
431        if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
432            mMediaPlayer.stop();
433            mMediaPlayer.release();
434            mMediaPlayer = createMediaPlayer();
435        }
436
437        updateConferenceable();
438    }
439
440    private void addCall(TestConnection connection) {
441        mCalls.add(connection);
442        updateConferenceable();
443    }
444
445    private void updateConferenceable() {
446        List<Connection> freeConnections = new ArrayList<>();
447        freeConnections.addAll(mCalls);
448        for (int i = 0; i < freeConnections.size(); i++) {
449            if (freeConnections.get(i).getConference() != null) {
450                freeConnections.remove(i);
451            }
452        }
453        for (int i = 0; i < freeConnections.size(); i++) {
454            Connection c = freeConnections.remove(i);
455            c.setConferenceableConnections(freeConnections);
456            freeConnections.add(i, c);
457        }
458    }
459
460    private MediaPlayer createMediaPlayer() {
461        // Prepare the media player to play a tone when there is a call.
462        MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop);
463        mediaPlayer.setLooping(true);
464        return mediaPlayer;
465    }
466
467    private static void log(String msg) {
468        Log.w("telecomtestcs", "[TestConnectionService] " + msg);
469    }
470
471    /**
472     * Generates a random phone number of format 555YXXX.  Where Y will be {@code 1} if the
473     * phone number is for a video call and {@code 0} for an audio call.  XXX is a randomly
474     * generated phone number.
475     *
476     * @param isVideo {@code True} if the call is a video call.
477     * @return The phone number.
478     */
479    private String getDummyNumber(boolean isVideo) {
480        int videoDigit = isVideo ? 1 : 0;
481        int number = mRandom.nextInt(999);
482        return String.format("555%s%03d", videoDigit, number);
483    }
484}
485
486