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