1/*
2 * Copyright (C) 2014 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 android.telecom;
18
19import android.net.Uri;
20import android.os.Bundle;
21import android.os.IBinder;
22import android.os.IBinder.DeathRecipient;
23import android.os.RemoteException;
24
25import com.android.internal.telecom.IConnectionService;
26import com.android.internal.telecom.IConnectionServiceAdapter;
27import com.android.internal.telecom.IVideoProvider;
28import com.android.internal.telecom.RemoteServiceCallback;
29
30import java.util.ArrayList;
31import java.util.HashMap;
32import java.util.HashSet;
33import java.util.Map;
34import java.util.Set;
35import java.util.List;
36import java.util.UUID;
37
38/**
39 * Remote connection service which other connection services can use to place calls on their behalf.
40 *
41 * @hide
42 */
43final class RemoteConnectionService {
44
45    // Note: Casting null to avoid ambiguous constructor reference.
46    private static final RemoteConnection NULL_CONNECTION =
47            new RemoteConnection("NULL", null, (ConnectionRequest) null);
48
49    private static final RemoteConference NULL_CONFERENCE =
50            new RemoteConference("NULL", null);
51
52    private final IConnectionServiceAdapter mServantDelegate = new IConnectionServiceAdapter() {
53        @Override
54        public void handleCreateConnectionComplete(
55                String id,
56                ConnectionRequest request,
57                ParcelableConnection parcel) {
58            RemoteConnection connection =
59                    findConnectionForAction(id, "handleCreateConnectionSuccessful");
60            if (connection != NULL_CONNECTION && mPendingConnections.contains(connection)) {
61                mPendingConnections.remove(connection);
62                // Unconditionally initialize the connection ...
63                connection.setConnectionCapabilities(parcel.getConnectionCapabilities());
64                connection.setConnectionProperties(parcel.getConnectionProperties());
65                if (parcel.getHandle() != null
66                    || parcel.getState() != Connection.STATE_DISCONNECTED) {
67                    connection.setAddress(parcel.getHandle(), parcel.getHandlePresentation());
68                }
69                if (parcel.getCallerDisplayName() != null
70                    || parcel.getState() != Connection.STATE_DISCONNECTED) {
71                    connection.setCallerDisplayName(
72                            parcel.getCallerDisplayName(),
73                            parcel.getCallerDisplayNamePresentation());
74                }
75                // Set state after handle so that the client can identify the connection.
76                if (parcel.getState() == Connection.STATE_DISCONNECTED) {
77                    connection.setDisconnected(parcel.getDisconnectCause());
78                } else {
79                    connection.setState(parcel.getState());
80                }
81                List<RemoteConnection> conferenceable = new ArrayList<>();
82                for (String confId : parcel.getConferenceableConnectionIds()) {
83                    if (mConnectionById.containsKey(confId)) {
84                        conferenceable.add(mConnectionById.get(confId));
85                    }
86                }
87                connection.setConferenceableConnections(conferenceable);
88                connection.setVideoState(parcel.getVideoState());
89                if (connection.getState() == Connection.STATE_DISCONNECTED) {
90                    // ... then, if it was created in a disconnected state, that indicates
91                    // failure on the providing end, so immediately mark it destroyed
92                    connection.setDestroyed();
93                }
94            }
95        }
96
97        @Override
98        public void setActive(String callId) {
99            if (mConnectionById.containsKey(callId)) {
100                findConnectionForAction(callId, "setActive")
101                        .setState(Connection.STATE_ACTIVE);
102            } else {
103                findConferenceForAction(callId, "setActive")
104                        .setState(Connection.STATE_ACTIVE);
105            }
106        }
107
108        @Override
109        public void setRinging(String callId) {
110            findConnectionForAction(callId, "setRinging")
111                    .setState(Connection.STATE_RINGING);
112        }
113
114        @Override
115        public void setDialing(String callId) {
116            findConnectionForAction(callId, "setDialing")
117                    .setState(Connection.STATE_DIALING);
118        }
119
120        @Override
121        public void setPulling(String callId) {
122            findConnectionForAction(callId, "setPulling")
123                    .setState(Connection.STATE_PULLING_CALL);
124        }
125
126        @Override
127        public void setDisconnected(String callId, DisconnectCause disconnectCause) {
128            if (mConnectionById.containsKey(callId)) {
129                findConnectionForAction(callId, "setDisconnected")
130                        .setDisconnected(disconnectCause);
131            } else {
132                findConferenceForAction(callId, "setDisconnected")
133                        .setDisconnected(disconnectCause);
134            }
135        }
136
137        @Override
138        public void setOnHold(String callId) {
139            if (mConnectionById.containsKey(callId)) {
140                findConnectionForAction(callId, "setOnHold")
141                        .setState(Connection.STATE_HOLDING);
142            } else {
143                findConferenceForAction(callId, "setOnHold")
144                        .setState(Connection.STATE_HOLDING);
145            }
146        }
147
148        @Override
149        public void setRingbackRequested(String callId, boolean ringing) {
150            findConnectionForAction(callId, "setRingbackRequested")
151                    .setRingbackRequested(ringing);
152        }
153
154        @Override
155        public void setConnectionCapabilities(String callId, int connectionCapabilities) {
156            if (mConnectionById.containsKey(callId)) {
157                findConnectionForAction(callId, "setConnectionCapabilities")
158                        .setConnectionCapabilities(connectionCapabilities);
159            } else {
160                findConferenceForAction(callId, "setConnectionCapabilities")
161                        .setConnectionCapabilities(connectionCapabilities);
162            }
163        }
164
165        @Override
166        public void setConnectionProperties(String callId, int connectionProperties) {
167            if (mConnectionById.containsKey(callId)) {
168                findConnectionForAction(callId, "setConnectionProperties")
169                        .setConnectionProperties(connectionProperties);
170            } else {
171                findConferenceForAction(callId, "setConnectionProperties")
172                        .setConnectionProperties(connectionProperties);
173            }
174        }
175
176        @Override
177        public void setIsConferenced(String callId, String conferenceCallId) {
178            // Note: callId should not be null; conferenceCallId may be null
179            RemoteConnection connection =
180                    findConnectionForAction(callId, "setIsConferenced");
181            if (connection != NULL_CONNECTION) {
182                if (conferenceCallId == null) {
183                    // 'connection' is being split from its conference
184                    if (connection.getConference() != null) {
185                        connection.getConference().removeConnection(connection);
186                    }
187                } else {
188                    RemoteConference conference =
189                            findConferenceForAction(conferenceCallId, "setIsConferenced");
190                    if (conference != NULL_CONFERENCE) {
191                        conference.addConnection(connection);
192                    }
193                }
194            }
195        }
196
197        @Override
198        public void setConferenceMergeFailed(String callId) {
199            // Nothing to do here.
200            // The event has already been handled and there is no state to update
201            // in the underlying connection or conference objects
202        }
203
204        @Override
205        public void addConferenceCall(
206                final String callId,
207                ParcelableConference parcel) {
208            RemoteConference conference = new RemoteConference(callId,
209                    mOutgoingConnectionServiceRpc);
210
211            for (String id : parcel.getConnectionIds()) {
212                RemoteConnection c = mConnectionById.get(id);
213                if (c != null) {
214                    conference.addConnection(c);
215                }
216            }
217            if (conference.getConnections().size() == 0) {
218                // A conference was created, but none of its connections are ones that have been
219                // created by, and therefore being tracked by, this remote connection service. It
220                // is of no interest to us.
221                Log.d(this, "addConferenceCall - skipping");
222                return;
223            }
224
225            conference.setState(parcel.getState());
226            conference.setConnectionCapabilities(parcel.getConnectionCapabilities());
227            conference.setConnectionProperties(parcel.getConnectionProperties());
228            conference.putExtras(parcel.getExtras());
229            mConferenceById.put(callId, conference);
230
231            // Stash the original connection ID as it exists in the source ConnectionService.
232            // Telecom will use this to avoid adding duplicates later.
233            // See comments on Connection.EXTRA_ORIGINAL_CONNECTION_ID for more information.
234            Bundle newExtras = new Bundle();
235            newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId);
236            conference.putExtras(newExtras);
237
238            conference.registerCallback(new RemoteConference.Callback() {
239                @Override
240                public void onDestroyed(RemoteConference c) {
241                    mConferenceById.remove(callId);
242                    maybeDisconnectAdapter();
243                }
244            });
245
246            mOurConnectionServiceImpl.addRemoteConference(conference);
247        }
248
249        @Override
250        public void removeCall(String callId) {
251            if (mConnectionById.containsKey(callId)) {
252                findConnectionForAction(callId, "removeCall")
253                        .setDestroyed();
254            } else {
255                findConferenceForAction(callId, "removeCall")
256                        .setDestroyed();
257            }
258        }
259
260        @Override
261        public void onPostDialWait(String callId, String remaining) {
262            findConnectionForAction(callId, "onPostDialWait")
263                    .setPostDialWait(remaining);
264        }
265
266        @Override
267        public void onPostDialChar(String callId, char nextChar) {
268            findConnectionForAction(callId, "onPostDialChar")
269                    .onPostDialChar(nextChar);
270        }
271
272        @Override
273        public void queryRemoteConnectionServices(RemoteServiceCallback callback) {
274            // Not supported from remote connection service.
275        }
276
277        @Override
278        public void setVideoProvider(String callId, IVideoProvider videoProvider) {
279            RemoteConnection.VideoProvider remoteVideoProvider = null;
280            if (videoProvider != null) {
281                remoteVideoProvider = new RemoteConnection.VideoProvider(videoProvider);
282            }
283            findConnectionForAction(callId, "setVideoProvider")
284                    .setVideoProvider(remoteVideoProvider);
285        }
286
287        @Override
288        public void setVideoState(String callId, int videoState) {
289            findConnectionForAction(callId, "setVideoState")
290                    .setVideoState(videoState);
291        }
292
293        @Override
294        public void setIsVoipAudioMode(String callId, boolean isVoip) {
295            findConnectionForAction(callId, "setIsVoipAudioMode")
296                    .setIsVoipAudioMode(isVoip);
297        }
298
299        @Override
300        public void setStatusHints(String callId, StatusHints statusHints) {
301            findConnectionForAction(callId, "setStatusHints")
302                    .setStatusHints(statusHints);
303        }
304
305        @Override
306        public void setAddress(String callId, Uri address, int presentation) {
307            findConnectionForAction(callId, "setAddress")
308                    .setAddress(address, presentation);
309        }
310
311        @Override
312        public void setCallerDisplayName(String callId, String callerDisplayName,
313                int presentation) {
314            findConnectionForAction(callId, "setCallerDisplayName")
315                    .setCallerDisplayName(callerDisplayName, presentation);
316        }
317
318        @Override
319        public IBinder asBinder() {
320            throw new UnsupportedOperationException();
321        }
322
323        @Override
324        public final void setConferenceableConnections(
325                String callId, List<String> conferenceableConnectionIds) {
326            List<RemoteConnection> conferenceable = new ArrayList<>();
327            for (String id : conferenceableConnectionIds) {
328                if (mConnectionById.containsKey(id)) {
329                    conferenceable.add(mConnectionById.get(id));
330                }
331            }
332
333            if (hasConnection(callId)) {
334                findConnectionForAction(callId, "setConferenceableConnections")
335                        .setConferenceableConnections(conferenceable);
336            } else {
337                findConferenceForAction(callId, "setConferenceableConnections")
338                        .setConferenceableConnections(conferenceable);
339            }
340        }
341
342        @Override
343        public void addExistingConnection(final String callId, ParcelableConnection connection) {
344            RemoteConnection remoteConnection = new RemoteConnection(callId,
345                    mOutgoingConnectionServiceRpc, connection);
346            mConnectionById.put(callId, remoteConnection);
347            remoteConnection.registerCallback(new RemoteConnection.Callback() {
348                @Override
349                public void onDestroyed(RemoteConnection connection) {
350                    mConnectionById.remove(callId);
351                    maybeDisconnectAdapter();
352                }
353            });
354            mOurConnectionServiceImpl.addRemoteExistingConnection(remoteConnection);
355        }
356
357        @Override
358        public void putExtras(String callId, Bundle extras) {
359            if (hasConnection(callId)) {
360                findConnectionForAction(callId, "putExtras").putExtras(extras);
361            } else {
362                findConferenceForAction(callId, "putExtras").putExtras(extras);
363            }
364        }
365
366        @Override
367        public void removeExtras(String callId, List<String> keys) {
368            if (hasConnection(callId)) {
369                findConnectionForAction(callId, "removeExtra").removeExtras(keys);
370            } else {
371                findConferenceForAction(callId, "removeExtra").removeExtras(keys);
372            }
373        }
374
375        @Override
376        public void onConnectionEvent(String callId, String event, Bundle extras) {
377            if (mConnectionById.containsKey(callId)) {
378                findConnectionForAction(callId, "onConnectionEvent").onConnectionEvent(event,
379                        extras);
380            }
381        }
382    };
383
384    private final ConnectionServiceAdapterServant mServant =
385            new ConnectionServiceAdapterServant(mServantDelegate);
386
387    private final DeathRecipient mDeathRecipient = new DeathRecipient() {
388        @Override
389        public void binderDied() {
390            for (RemoteConnection c : mConnectionById.values()) {
391                c.setDestroyed();
392            }
393            for (RemoteConference c : mConferenceById.values()) {
394                c.setDestroyed();
395            }
396            mConnectionById.clear();
397            mConferenceById.clear();
398            mPendingConnections.clear();
399            mOutgoingConnectionServiceRpc.asBinder().unlinkToDeath(mDeathRecipient, 0);
400        }
401    };
402
403    private final IConnectionService mOutgoingConnectionServiceRpc;
404    private final ConnectionService mOurConnectionServiceImpl;
405    private final Map<String, RemoteConnection> mConnectionById = new HashMap<>();
406    private final Map<String, RemoteConference> mConferenceById = new HashMap<>();
407    private final Set<RemoteConnection> mPendingConnections = new HashSet<>();
408
409    RemoteConnectionService(
410            IConnectionService outgoingConnectionServiceRpc,
411            ConnectionService ourConnectionServiceImpl) throws RemoteException {
412        mOutgoingConnectionServiceRpc = outgoingConnectionServiceRpc;
413        mOutgoingConnectionServiceRpc.asBinder().linkToDeath(mDeathRecipient, 0);
414        mOurConnectionServiceImpl = ourConnectionServiceImpl;
415    }
416
417    @Override
418    public String toString() {
419        return "[RemoteCS - " + mOutgoingConnectionServiceRpc.asBinder().toString() + "]";
420    }
421
422    final RemoteConnection createRemoteConnection(
423            PhoneAccountHandle connectionManagerPhoneAccount,
424            ConnectionRequest request,
425            boolean isIncoming) {
426        final String id = UUID.randomUUID().toString();
427        final ConnectionRequest newRequest = new ConnectionRequest(
428                request.getAccountHandle(),
429                request.getAddress(),
430                request.getExtras(),
431                request.getVideoState());
432        try {
433            if (mConnectionById.isEmpty()) {
434                mOutgoingConnectionServiceRpc.addConnectionServiceAdapter(mServant.getStub());
435            }
436            RemoteConnection connection =
437                    new RemoteConnection(id, mOutgoingConnectionServiceRpc, newRequest);
438            mPendingConnections.add(connection);
439            mConnectionById.put(id, connection);
440            mOutgoingConnectionServiceRpc.createConnection(
441                    connectionManagerPhoneAccount,
442                    id,
443                    newRequest,
444                    isIncoming,
445                    false /* isUnknownCall */);
446            connection.registerCallback(new RemoteConnection.Callback() {
447                @Override
448                public void onDestroyed(RemoteConnection connection) {
449                    mConnectionById.remove(id);
450                    maybeDisconnectAdapter();
451                }
452            });
453            return connection;
454        } catch (RemoteException e) {
455            return RemoteConnection.failure(
456                    new DisconnectCause(DisconnectCause.ERROR, e.toString()));
457        }
458    }
459
460    private boolean hasConnection(String callId) {
461        return mConnectionById.containsKey(callId);
462    }
463
464    private RemoteConnection findConnectionForAction(
465            String callId, String action) {
466        if (mConnectionById.containsKey(callId)) {
467            return mConnectionById.get(callId);
468        }
469        Log.w(this, "%s - Cannot find Connection %s", action, callId);
470        return NULL_CONNECTION;
471    }
472
473    private RemoteConference findConferenceForAction(
474            String callId, String action) {
475        if (mConferenceById.containsKey(callId)) {
476            return mConferenceById.get(callId);
477        }
478        Log.w(this, "%s - Cannot find Conference %s", action, callId);
479        return NULL_CONFERENCE;
480    }
481
482    private void maybeDisconnectAdapter() {
483        if (mConnectionById.isEmpty() && mConferenceById.isEmpty()) {
484            try {
485                mOutgoingConnectionServiceRpc.removeConnectionServiceAdapter(mServant.getStub());
486            } catch (RemoteException e) {
487            }
488        }
489    }
490}
491