TestConnectionService.java revision b2dfc423ce184eb2c1411725f317338d1581cd96
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_MUTE;
162            capabilities |= CAPABILITY_SUPPORT_HOLD;
163            capabilities |= CAPABILITY_HOLD;
164            setConnectionCapabilities(capabilities);
165
166            LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
167                    mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS));
168            final IntentFilter filter =
169                    new IntentFilter(TestCallActivity.ACTION_SEND_UPGRADE_REQUEST);
170            filter.addDataScheme("int");
171            LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
172                    mUpgradeRequestReceiver, filter);
173        }
174
175        void startOutgoing() {
176            setDialing();
177            mHandler.postDelayed(new Runnable() {
178                @Override
179                public void run() {
180                    setActive();
181                    activateCall(TestConnection.this);
182                }
183            }, 4000);
184        }
185
186        /** ${inheritDoc} */
187        @Override
188        public void onAbort() {
189            destroyCall(this);
190            destroy();
191        }
192
193        /** ${inheritDoc} */
194        @Override
195        public void onAnswer(int videoState) {
196            setVideoState(videoState);
197            activateCall(this);
198            setActive();
199            updateConferenceable();
200        }
201
202        /** ${inheritDoc} */
203        @Override
204        public void onPlayDtmfTone(char c) {
205            if (c == '1') {
206                setDialing();
207            }
208        }
209
210        /** ${inheritDoc} */
211        @Override
212        public void onStopDtmfTone() { }
213
214        /** ${inheritDoc} */
215        @Override
216        public void onDisconnect() {
217            setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
218            destroyCall(this);
219            destroy();
220        }
221
222        /** ${inheritDoc} */
223        @Override
224        public void onHold() {
225            setOnHold();
226        }
227
228        /** ${inheritDoc} */
229        @Override
230        public void onReject() {
231            setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
232            destroyCall(this);
233            destroy();
234        }
235
236        /** ${inheritDoc} */
237        @Override
238        public void onUnhold() {
239            setActive();
240        }
241
242        @Override
243        public void onAudioStateChanged(AudioState state) { }
244
245        public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
246            mTestVideoCallProvider = testVideoCallProvider;
247        }
248
249        public void cleanup() {
250            LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
251                    mHangupReceiver);
252            LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
253                    mUpgradeRequestReceiver);
254        }
255
256        /**
257         * Stops playback of test videos.
258         */
259        private void stopAndCleanupMedia() {
260            if (mTestVideoCallProvider != null) {
261                mTestVideoCallProvider.stopAndCleanupMedia();
262                mTestVideoCallProvider.stopCamera();
263            }
264        }
265    }
266
267    private final List<TestConnection> mCalls = new ArrayList<>();
268    private final Handler mHandler = new Handler();
269
270    /** Used to play an audio tone during a call. */
271    private MediaPlayer mMediaPlayer;
272
273    @Override
274    public boolean onUnbind(Intent intent) {
275        log("onUnbind");
276        mMediaPlayer = null;
277        return super.onUnbind(intent);
278    }
279
280    @Override
281    public void onConference(Connection a, Connection b) {
282        addConference(new TestConference(a, b));
283    }
284
285    @Override
286    public Connection onCreateOutgoingConnection(
287            PhoneAccountHandle connectionManagerAccount,
288            final ConnectionRequest originalRequest) {
289
290        final Uri handle = originalRequest.getAddress();
291        String number = originalRequest.getAddress().getSchemeSpecificPart();
292        log("call, number: " + number);
293
294        // Crash on 555-DEAD to test call service crashing.
295        if ("5550340".equals(number)) {
296            throw new RuntimeException("Goodbye, cruel world.");
297        }
298
299        Bundle extras = originalRequest.getExtras();
300        String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE);
301        Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS);
302
303        log("gateway package [" + gatewayPackage + "], original handle [" +
304                originalHandle + "]");
305
306        final TestConnection connection = new TestConnection(false /* isIncoming */);
307        connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED);
308
309        // If the number starts with 555, then we handle it ourselves. If not, then we
310        // use a remote connection service.
311        // TODO: Have a special phone number to test the account-picker dialog flow.
312        if (number != null && number.startsWith("555")) {
313            // Normally we would use the original request as is, but for testing purposes, we are
314            // adding ".." to the end of the number to follow its path more easily through the logs.
315            final ConnectionRequest request = new ConnectionRequest(
316                    originalRequest.getAccountHandle(),
317                    Uri.fromParts(handle.getScheme(),
318                    handle.getSchemeSpecificPart() + "..", ""),
319                    originalRequest.getExtras(),
320                    originalRequest.getVideoState());
321            connection.setVideoState(originalRequest.getVideoState());
322            addVideoProvider(connection);
323            addCall(connection);
324            connection.startOutgoing();
325
326            for (Connection c : getAllConnections()) {
327                c.setOnHold();
328            }
329        } else {
330            log("Not a test number");
331        }
332        return connection;
333    }
334
335    @Override
336    public Connection onCreateIncomingConnection(
337            PhoneAccountHandle connectionManagerAccount,
338            final ConnectionRequest request) {
339        PhoneAccountHandle accountHandle = request.getAccountHandle();
340        ComponentName componentName = new ComponentName(this, TestConnectionService.class);
341
342        if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
343            final TestConnection connection = new TestConnection(true);
344            // Get the stashed intent extra that determines if this is a video call or audio call.
345            Bundle extras = request.getExtras();
346            boolean isVideoCall = extras.getBoolean(EXTRA_IS_VIDEO_CALL);
347            Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
348
349            // Use dummy number for testing incoming calls.
350            Uri address = providedHandle == null ?
351                    Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(isVideoCall), null)
352                    : providedHandle;
353
354            int videoState = isVideoCall ?
355                    VideoProfile.VideoState.BIDIRECTIONAL :
356                    VideoProfile.VideoState.AUDIO_ONLY;
357            connection.setVideoState(videoState);
358            connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
359
360            addVideoProvider(connection);
361
362            addCall(connection);
363
364            ConnectionRequest newRequest = new ConnectionRequest(
365                    request.getAccountHandle(),
366                    address,
367                    request.getExtras(),
368                    videoState);
369            connection.setVideoState(videoState);
370            return connection;
371        } else {
372            return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
373                    "Invalid inputs: " + accountHandle + " " + componentName));
374        }
375    }
376
377    @Override
378    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
379            final ConnectionRequest request) {
380        PhoneAccountHandle accountHandle = request.getAccountHandle();
381        ComponentName componentName = new ComponentName(this, TestConnectionService.class);
382        if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
383            final TestConnection connection = new TestConnection(false);
384            final Bundle extras = request.getExtras();
385            final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
386
387            Uri handle = providedHandle == null ?
388                    Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null)
389                    : providedHandle;
390
391            connection.setAddress(handle,  TelecomManager.PRESENTATION_ALLOWED);
392            connection.setDialing();
393
394            addCall(connection);
395            return connection;
396        } else {
397            return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
398                    "Invalid inputs: " + accountHandle + " " + componentName));
399        }
400    }
401
402    private void addVideoProvider(TestConnection connection) {
403        TestVideoProvider testVideoCallProvider =
404                new TestVideoProvider(getApplicationContext(), connection);
405        connection.setVideoProvider(testVideoCallProvider);
406
407        // Keep reference to original so we can clean up the media players later.
408        connection.setTestVideoCallProvider(testVideoCallProvider);
409    }
410
411    private void activateCall(TestConnection connection) {
412        if (mMediaPlayer == null) {
413            mMediaPlayer = createMediaPlayer();
414        }
415        if (!mMediaPlayer.isPlaying()) {
416            mMediaPlayer.start();
417        }
418    }
419
420    private void destroyCall(TestConnection connection) {
421        connection.cleanup();
422        mCalls.remove(connection);
423
424        // Ensure any playing media and camera resources are released.
425        connection.stopAndCleanupMedia();
426
427        // Stops audio if there are no more calls.
428        if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
429            mMediaPlayer.stop();
430            mMediaPlayer.release();
431            mMediaPlayer = createMediaPlayer();
432        }
433
434        updateConferenceable();
435    }
436
437    private void addCall(TestConnection connection) {
438        mCalls.add(connection);
439        updateConferenceable();
440    }
441
442    private void updateConferenceable() {
443        List<Connection> freeConnections = new ArrayList<>();
444        freeConnections.addAll(mCalls);
445        for (int i = 0; i < freeConnections.size(); i++) {
446            if (freeConnections.get(i).getConference() != null) {
447                freeConnections.remove(i);
448            }
449        }
450        for (int i = 0; i < freeConnections.size(); i++) {
451            Connection c = freeConnections.remove(i);
452            c.setConferenceableConnections(freeConnections);
453            freeConnections.add(i, c);
454        }
455    }
456
457    private MediaPlayer createMediaPlayer() {
458        // Prepare the media player to play a tone when there is a call.
459        MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop);
460        mediaPlayer.setLooping(true);
461        return mediaPlayer;
462    }
463
464    private static void log(String msg) {
465        Log.w("telecomtestcs", "[TestConnectionService] " + msg);
466    }
467
468    /**
469     * Generates a random phone number of format 555YXXX.  Where Y will be {@code 1} if the
470     * phone number is for a video call and {@code 0} for an audio call.  XXX is a randomly
471     * generated phone number.
472     *
473     * @param isVideo {@code True} if the call is a video call.
474     * @return The phone number.
475     */
476    private String getDummyNumber(boolean isVideo) {
477        int videoDigit = isVideo ? 1 : 0;
478        int number = mRandom.nextInt(999);
479        return String.format("555%s%03d", videoDigit, number);
480    }
481}
482
483