blob: 4b69a3c8247acedf4243a6ae61be34777b2afc45 [file] [log] [blame]
Ihab Awad542e0ea2014-05-16 10:22:16 -07001/*
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.telecomm;
18
Santos Cordon52d8a152014-06-17 19:08:45 -070019import android.content.ComponentName;
Ihab Awad542e0ea2014-05-16 10:22:16 -070020import android.net.Uri;
21import android.os.Bundle;
Santos Cordon52d8a152014-06-17 19:08:45 -070022
23import android.os.Handler;
24import android.os.IBinder;
25import android.os.Looper;
Ihab Awadfc91b7d2014-06-03 18:40:45 -070026import android.telephony.DisconnectCause;
Ihab Awad542e0ea2014-05-16 10:22:16 -070027
Santos Cordon52d8a152014-06-17 19:08:45 -070028import com.android.internal.telecomm.ICallService;
29import com.android.internal.telecomm.RemoteServiceCallback;
30
Santos Cordonb6939982014-06-04 20:20:58 -070031import java.util.Collection;
Ihab Awad542e0ea2014-05-16 10:22:16 -070032import java.util.HashMap;
Santos Cordon52d8a152014-06-17 19:08:45 -070033import java.util.List;
Ihab Awad542e0ea2014-05-16 10:22:16 -070034import java.util.Map;
35
36/**
37 * A {@link android.app.Service} that provides telephone connections to
38 * processes running on an Android device.
39 */
40public abstract class ConnectionService extends CallService {
Ihab Awad542e0ea2014-05-16 10:22:16 -070041 // Flag controlling whether PII is emitted into the logs
Ihab Awad60ac30b2014-05-20 22:32:12 -070042 private static final boolean PII_DEBUG = Log.isLoggable(android.util.Log.DEBUG);
Ihab Awad542e0ea2014-05-16 10:22:16 -070043 private static final Connection NULL_CONNECTION = new Connection() {};
44
45 // Mappings from Connections to IDs as understood by the current CallService implementation
46 private final Map<String, Connection> mConnectionById = new HashMap<>();
47 private final Map<Connection, String> mIdByConnection = new HashMap<>();
Santos Cordon52d8a152014-06-17 19:08:45 -070048 private final RemoteConnectionManager mRemoteConnectionManager = new RemoteConnectionManager();
49 private final Handler mHandler = new Handler(Looper.getMainLooper());
50
51 private SimpleResponse<Uri, List<Subscription>> mSubscriptionLookupResponse;
52 private Uri mSubscriptionLookupHandle;
53 private boolean mAreSubscriptionsInitialized = false;
Ihab Awad542e0ea2014-05-16 10:22:16 -070054
55 private final Connection.Listener mConnectionListener = new Connection.Listener() {
56 @Override
57 public void onStateChanged(Connection c, int state) {
58 String id = mIdByConnection.get(c);
Ihab Awad42b30e12014-05-22 09:49:34 -070059 Log.d(this, "Adapter set state %s %s", id, Connection.stateToString(state));
Ihab Awad542e0ea2014-05-16 10:22:16 -070060 switch (state) {
61 case Connection.State.ACTIVE:
62 getAdapter().setActive(id);
63 break;
64 case Connection.State.DIALING:
65 getAdapter().setDialing(id);
66 break;
67 case Connection.State.DISCONNECTED:
68 // Handled in onDisconnected()
69 break;
70 case Connection.State.HOLDING:
71 getAdapter().setOnHold(id);
72 break;
73 case Connection.State.NEW:
74 // Nothing to tell Telecomm
75 break;
76 case Connection.State.RINGING:
77 getAdapter().setRinging(id);
78 break;
79 }
80 }
81
82 @Override
83 public void onDisconnected(Connection c, int cause, String message) {
84 String id = mIdByConnection.get(c);
Ihab Awad60ac30b2014-05-20 22:32:12 -070085 Log.d(this, "Adapter set disconnected %d %s", cause, message);
Ihab Awad542e0ea2014-05-16 10:22:16 -070086 getAdapter().setDisconnected(id, cause, message);
87 }
88
89 @Override
90 public void onHandleChanged(Connection c, Uri newHandle) {
91 // TODO: Unsupported yet
92 }
93
94 @Override
95 public void onAudioStateChanged(Connection c, CallAudioState state) {
96 // TODO: Unsupported yet
97 }
98
99 @Override
100 public void onSignalChanged(Connection c, Bundle details) {
101 // TODO: Unsupported yet
102 }
103
104 @Override
105 public void onDestroyed(Connection c) {
106 removeConnection(c);
107 }
Ihab Awadf8358972014-05-28 16:46:42 -0700108
109 @Override
110 public void onRequestingRingback(Connection c, boolean ringback) {
111 String id = mIdByConnection.get(c);
112 Log.d(this, "Adapter onRingback %b", ringback);
113 getAdapter().setRequestingRingback(id, ringback);
114 }
Santos Cordonb6939982014-06-04 20:20:58 -0700115
116 @Override
117 public void onConferenceCapableChanged(Connection c, boolean isConferenceCapable) {
118 String id = mIdByConnection.get(c);
119 getAdapter().setCanConference(id, isConferenceCapable);
120 }
121
122 /** ${inheritDoc} */
123 @Override
124 public void onParentConnectionChanged(Connection c, Connection parent) {
125 String id = mIdByConnection.get(c);
126 String parentId = parent == null ? null : mIdByConnection.get(parent);
127 getAdapter().setIsConferenced(id, parentId);
128 }
Ihab Awad542e0ea2014-05-16 10:22:16 -0700129 };
130
131 @Override
Ihab Awad542e0ea2014-05-16 10:22:16 -0700132 public final void call(final CallInfo callInfo) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700133 Log.d(this, "call %s", callInfo);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700134 onCreateConnections(
135 new ConnectionRequest(
Ihab Awadfc91b7d2014-06-03 18:40:45 -0700136 callInfo.getId(),
Ihab Awad542e0ea2014-05-16 10:22:16 -0700137 callInfo.getHandle(),
138 callInfo.getExtras()),
139 new Response<ConnectionRequest, Connection>() {
140 @Override
141 public void onResult(ConnectionRequest request, Connection... result) {
Santos Cordonb6939982014-06-04 20:20:58 -0700142 if (result != null && result.length != 1) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700143 Log.d(this, "adapter handleFailedOutgoingCall %s", callInfo);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700144 getAdapter().handleFailedOutgoingCall(
Ihab Awadfc91b7d2014-06-03 18:40:45 -0700145 request,
146 DisconnectCause.ERROR_UNSPECIFIED,
Ihab Awad542e0ea2014-05-16 10:22:16 -0700147 "Created " + result.length + " Connections, expected 1");
148 for (Connection c : result) {
149 c.abort();
150 }
151 } else {
Evan Charlton6dea4ac2014-06-03 14:07:13 -0700152 Log.d(this, "adapter handleSuccessfulOutgoingCall %s",
153 callInfo.getId());
Ihab Awad542e0ea2014-05-16 10:22:16 -0700154 getAdapter().handleSuccessfulOutgoingCall(callInfo.getId());
Santos Cordonb6939982014-06-04 20:20:58 -0700155 addConnection(callInfo.getId(), result[0]);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700156 }
157 }
158
159 @Override
Ihab Awadfc91b7d2014-06-03 18:40:45 -0700160 public void onError(ConnectionRequest request, int code, String msg) {
161 getAdapter().handleFailedOutgoingCall(request, code, msg);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700162 }
163 }
164 );
165 }
166
167 @Override
168 public final void abort(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700169 Log.d(this, "abort %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700170 findConnectionForAction(callId, "abort").abort();
171 }
172
173 @Override
174 public final void setIncomingCallId(final String callId, Bundle extras) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700175 Log.d(this, "setIncomingCallId %s %s", callId, extras);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700176 onCreateIncomingConnection(
177 new ConnectionRequest(
Ihab Awadfc91b7d2014-06-03 18:40:45 -0700178 callId,
Ihab Awad542e0ea2014-05-16 10:22:16 -0700179 null, // TODO: Can we obtain this from "extras"?
180 extras),
181 new Response<ConnectionRequest, Connection>() {
182 @Override
183 public void onResult(ConnectionRequest request, Connection... result) {
Santos Cordonb6939982014-06-04 20:20:58 -0700184 if (result != null && result.length != 1) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700185 Log.d(this, "adapter handleFailedOutgoingCall %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700186 getAdapter().handleFailedOutgoingCall(
Ihab Awadfc91b7d2014-06-03 18:40:45 -0700187 request,
188 DisconnectCause.ERROR_UNSPECIFIED,
Ihab Awad542e0ea2014-05-16 10:22:16 -0700189 "Created " + result.length + " Connections, expected 1");
190 for (Connection c : result) {
191 c.abort();
192 }
193 } else {
194 addConnection(callId, result[0]);
Ihab Awad60ac30b2014-05-20 22:32:12 -0700195 Log.d(this, "adapter notifyIncomingCall %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700196 // TODO: Uri.EMPTY is because CallInfo crashes when Parceled with a
197 // null URI ... need to fix that at its cause!
198 getAdapter().notifyIncomingCall(new CallInfo(
199 callId,
200 connectionStateToCallState(result[0].getState()),
201 request.getHandle() /* result[0].getHandle() == null
202 ? Uri.EMPTY : result[0].getHandle() */));
203 }
204 }
205
206 @Override
Ihab Awadfc91b7d2014-06-03 18:40:45 -0700207 public void onError(ConnectionRequest request, int code, String msg) {
208 Log.d(this, "adapter failed setIncomingCallId %s %d %s",
209 request, code, msg);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700210 }
211 }
212 );
213 }
214
215 @Override
216 public final void answer(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700217 Log.d(this, "answer %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700218 findConnectionForAction(callId, "answer").answer();
219 }
220
221 @Override
222 public final void reject(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700223 Log.d(this, "reject %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700224 findConnectionForAction(callId, "reject").reject();
225 }
226
227 @Override
228 public final void disconnect(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700229 Log.d(this, "disconnect %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700230 findConnectionForAction(callId, "disconnect").disconnect();
231 }
232
233 @Override
234 public final void hold(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700235 Log.d(this, "hold %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700236 findConnectionForAction(callId, "hold").hold();
237 }
238
239 @Override
240 public final void unhold(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700241 Log.d(this, "unhold %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700242 findConnectionForAction(callId, "unhold").unhold();
243 }
244
245 @Override
246 public final void playDtmfTone(String callId, char digit) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700247 Log.d(this, "playDtmfTone %s %c", callId, digit);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700248 findConnectionForAction(callId, "playDtmfTone").playDtmfTone(digit);
249 }
250
251 @Override
252 public final void stopDtmfTone(String callId) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700253 Log.d(this, "stopDtmfTone %s", callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700254 findConnectionForAction(callId, "stopDtmfTone").stopDtmfTone();
255 }
256
257 @Override
258 public final void onAudioStateChanged(String callId, CallAudioState audioState) {
Ihab Awad60ac30b2014-05-20 22:32:12 -0700259 Log.d(this, "onAudioStateChanged %s %s", callId, audioState);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700260 findConnectionForAction(callId, "onAudioStateChanged").setAudioState(audioState);
261 }
262
Santos Cordon980acb92014-05-31 10:31:19 -0700263 /** @hide */
264 @Override
Santos Cordonb6939982014-06-04 20:20:58 -0700265 public final void conference(final String conferenceCallId, String callId) {
266 Log.d(this, "conference %s, %s", conferenceCallId, callId);
Santos Cordon980acb92014-05-31 10:31:19 -0700267
Santos Cordonb6939982014-06-04 20:20:58 -0700268 Connection connection = findConnectionForAction(callId, "conference");
269 if (connection == NULL_CONNECTION) {
270 Log.w(this, "Connection missing in conference request %s.", callId);
271 return;
Santos Cordon980acb92014-05-31 10:31:19 -0700272 }
273
Santos Cordonb6939982014-06-04 20:20:58 -0700274 onCreateConferenceConnection(conferenceCallId, connection,
275 new Response<String, Connection>() {
276 /** ${inheritDoc} */
277 @Override
278 public void onResult(String ignored, Connection... result) {
279 Log.d(this, "onCreateConference.Response %s", (Object[]) result);
280 if (result != null && result.length == 1) {
281 Connection conferenceConnection = result[0];
282 if (!mIdByConnection.containsKey(conferenceConnection)) {
283 Log.v(this, "sending new conference call %s", conferenceCallId);
284 getAdapter().addConferenceCall(conferenceCallId);
285 addConnection(conferenceCallId, conferenceConnection);
286 }
287 }
288 }
289
290 /** ${inheritDoc} */
291 @Override
292 public void onError(String request, int code, String reason) {
293 // no-op
294 }
295 });
Santos Cordon980acb92014-05-31 10:31:19 -0700296 }
297
298 /** @hide */
299 @Override
Santos Cordonb6939982014-06-04 20:20:58 -0700300 public final void splitFromConference(String callId) {
301 Log.d(this, "splitFromConference(%s)", callId);
Santos Cordon980acb92014-05-31 10:31:19 -0700302
303 Connection connection = findConnectionForAction(callId, "splitFromConference");
304 if (connection == NULL_CONNECTION) {
305 Log.w(this, "Connection missing in conference request %s.", callId);
306 return;
307 }
308
309 // TODO(santoscordon): Find existing conference call and invoke split(connection).
310 }
311
Evan Charlton6dea4ac2014-06-03 14:07:13 -0700312 @Override
313 public final void onPostDialContinue(String callId, boolean proceed) {
314 Log.d(this, "onPostDialContinue(%s)", callId);
315
316 Connection connection = findConnectionForAction(callId, "onPostDialContinue");
317 if (connection == NULL_CONNECTION) {
318 Log.w(this, "Connection missing in post-dial request %s.", callId);
319 return;
320 }
321 connection.onPostDialContinue(proceed);
322 }
323
324 @Override
325 public final void onPostDialWait(Connection conn, String remaining) {
326 Log.d(this, "onPostDialWait(%s, %s)", conn, remaining);
327
328 getAdapter().onPostDialWait(mIdByConnection.get(conn), remaining);
329 }
330
Ihab Awad542e0ea2014-05-16 10:22:16 -0700331 /**
Santos Cordon52d8a152014-06-17 19:08:45 -0700332 * @hide
333 */
334 @Override
335 protected void onAdapterAttached(CallServiceAdapter adapter) {
336 if (mAreSubscriptionsInitialized) {
337 // No need to query again if we already did it.
338 return;
339 }
340
341 getAdapter().queryRemoteConnectionServices(new RemoteServiceCallback.Stub() {
342 @Override
343 public void onResult(
344 final List<ComponentName> componentNames,
345 final List<IBinder> callServices) {
346 mHandler.post(new Runnable() {
347 @Override public void run() {
348 for (int i = 0; i < componentNames.size() && i < callServices.size(); i++) {
349 mRemoteConnectionManager.addConnectionService(
350 componentNames.get(i),
351 ICallService.Stub.asInterface(callServices.get(i)));
352 }
353 mAreSubscriptionsInitialized = true;
354 Log.d(this, "remote call services found: " + callServices);
355 maybeRespondToSubscriptionLookup();
356 }
357 });
358 }
359
360 @Override
361 public void onError() {
362 mHandler.post(new Runnable() {
363 @Override public void run() {
364 mAreSubscriptionsInitialized = true;
365 maybeRespondToSubscriptionLookup();
366 }
367 });
368 }
369 });
370 }
371
372 public void lookupRemoteSubscriptions(
373 Uri handle, SimpleResponse<Uri, List<Subscription>> response) {
374 mSubscriptionLookupResponse = response;
375 mSubscriptionLookupHandle = handle;
376 maybeRespondToSubscriptionLookup();
377 }
378
379 public void maybeRespondToSubscriptionLookup() {
380 if (mAreSubscriptionsInitialized && mSubscriptionLookupResponse != null) {
381 mSubscriptionLookupResponse.onResult(
382 mSubscriptionLookupHandle,
383 mRemoteConnectionManager.getSubscriptions(mSubscriptionLookupHandle));
384
385 mSubscriptionLookupHandle = null;
386 mSubscriptionLookupResponse = null;
387 }
388 }
389
390 public void createRemoteOutgoingConnection(
391 ConnectionRequest request,
392 SimpleResponse<ConnectionRequest, RemoteConnection> response) {
393 mRemoteConnectionManager.createOutgoingConnection(request, response);
394 }
395
396 /**
Santos Cordonb6939982014-06-04 20:20:58 -0700397 * Returns all connections currently associated with this connection service.
398 */
399 public Collection<Connection> getAllConnections() {
400 return mConnectionById.values();
401 }
402
403 /**
Ihab Awad542e0ea2014-05-16 10:22:16 -0700404 * Create a Connection given a request.
405 *
406 * @param request Data encapsulating details of the desired Connection.
407 * @param callback A callback for providing the result.
408 */
409 public void onCreateConnections(
410 ConnectionRequest request,
411 Response<ConnectionRequest, Connection> callback) {}
412
413 /**
Santos Cordonb6939982014-06-04 20:20:58 -0700414 * Returns a new or existing conference connection when the the user elects to convert the
415 * specified connection into a conference call. The specified connection can be any connection
416 * which had previously specified itself as conference-capable including both simple connections
417 * and connections previously returned from this method.
418 *
419 * @param connection The connection from which the user opted to start a conference call.
420 * @param token The token to be passed into the response callback.
421 * @param callback The callback for providing the potentially-new conference connection.
422 */
423 public void onCreateConferenceConnection(
424 String token,
425 Connection connection,
426 Response<String, Connection> callback) {}
427
428 /**
Ihab Awad542e0ea2014-05-16 10:22:16 -0700429 * Create a Connection to match an incoming connection notification.
430 *
Ihab Awadc0677542014-06-10 13:29:47 -0700431 * IMPORTANT: If the incoming connection has a phone number (or other handle) that the user
432 * is not supposed to be able to see (e.g. it is PRESENTATION_RESTRICTED), then a compliant
433 * ConnectionService implementation MUST NOT reveal this phone number as part of the Intent
434 * it sends to notify Telecomm of an incoming connection.
435 *
Ihab Awad542e0ea2014-05-16 10:22:16 -0700436 * @param request Data encapsulating details of the desired Connection.
437 * @param callback A callback for providing the result.
438 */
439 public void onCreateIncomingConnection(
440 ConnectionRequest request,
441 Response<ConnectionRequest, Connection> callback) {}
442
Santos Cordonb6939982014-06-04 20:20:58 -0700443 /**
444 * Notifies that a connection has been added to this connection service and sent to Telecomm.
445 *
446 * @param connection The connection which was added.
447 */
448 public void onConnectionAdded(Connection connection) {}
449
450 /**
451 * Notified that a connection has been removed from this connection service.
452 *
453 * @param connection The connection which was removed.
454 */
455 public void onConnectionRemoved(Connection connection) {}
456
Ihab Awad542e0ea2014-05-16 10:22:16 -0700457 static String toLogSafePhoneNumber(String number) {
458 // For unknown number, log empty string.
459 if (number == null) {
460 return "";
461 }
462
463 if (PII_DEBUG) {
464 // When PII_DEBUG is true we emit PII.
465 return number;
466 }
467
468 // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
469 // sanitized phone numbers.
470 StringBuilder builder = new StringBuilder();
471 for (int i = 0; i < number.length(); i++) {
472 char c = number.charAt(i);
473 if (c == '-' || c == '@' || c == '.') {
474 builder.append(c);
475 } else {
476 builder.append('x');
477 }
478 }
479 return builder.toString();
480 }
481
482 private CallState connectionStateToCallState(int connectionState) {
483 switch (connectionState) {
484 case Connection.State.NEW:
485 return CallState.NEW;
486 case Connection.State.RINGING:
487 return CallState.RINGING;
488 case Connection.State.DIALING:
489 return CallState.DIALING;
490 case Connection.State.ACTIVE:
491 return CallState.ACTIVE;
492 case Connection.State.HOLDING:
493 return CallState.ON_HOLD;
494 case Connection.State.DISCONNECTED:
495 return CallState.DISCONNECTED;
496 default:
Ihab Awad60ac30b2014-05-20 22:32:12 -0700497 Log.wtf(this, "Unknown Connection.State %d", connectionState);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700498 return CallState.NEW;
499 }
500 }
501
502 private void addConnection(String callId, Connection connection) {
503 mConnectionById.put(callId, connection);
504 mIdByConnection.put(connection, callId);
505 connection.addConnectionListener(mConnectionListener);
Santos Cordonb6939982014-06-04 20:20:58 -0700506 onConnectionAdded(connection);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700507 }
508
509 private void removeConnection(Connection connection) {
510 connection.removeConnectionListener(mConnectionListener);
511 mConnectionById.remove(mIdByConnection.get(connection));
512 mIdByConnection.remove(connection);
Santos Cordonb6939982014-06-04 20:20:58 -0700513 onConnectionRemoved(connection);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700514 }
515
516 private Connection findConnectionForAction(String callId, String action) {
517 if (mConnectionById.containsKey(callId)) {
518 return mConnectionById.get(callId);
519 }
Ihab Awad60ac30b2014-05-20 22:32:12 -0700520 Log.w(this, "%s - Cannot find Connection %s", action, callId);
Ihab Awad542e0ea2014-05-16 10:22:16 -0700521 return NULL_CONNECTION;
522 }
Santos Cordon980acb92014-05-31 10:31:19 -0700523}