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