• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.server.telecom;
18 
19 import android.Manifest;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.ServiceConnection;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.content.pm.ServiceInfo;
27 import android.content.res.Resources;
28 import android.net.Uri;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.os.UserHandle;
32 import android.telecom.AudioState;
33 import android.telecom.CallProperties;
34 import android.telecom.CallState;
35 import android.telecom.InCallService;
36 import android.telecom.ParcelableCall;
37 import android.telecom.PhoneCapabilities;
38 import android.telecom.TelecomManager;
39 import android.util.ArrayMap;
40 
41 // TODO: Needed for move to system service: import com.android.internal.R;
42 import com.android.internal.telecom.IInCallService;
43 import com.google.common.collect.ImmutableCollection;
44 
45 import java.util.ArrayList;
46 import java.util.Iterator;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.concurrent.ConcurrentHashMap;
50 
51 /**
52  * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
53  * can send updates to the in-call app. This class is created and owned by CallsManager and retains
54  * a binding to the {@link IInCallService} (implemented by the in-call app).
55  */
56 public final class InCallController extends CallsManagerListenerBase {
57     /**
58      * Used to bind to the in-call app and triggers the start of communication between
59      * this class and in-call app.
60      */
61     private class InCallServiceConnection implements ServiceConnection {
62         /** {@inheritDoc} */
onServiceConnected(ComponentName name, IBinder service)63         @Override public void onServiceConnected(ComponentName name, IBinder service) {
64             Log.d(this, "onServiceConnected: %s", name);
65             onConnected(name, service);
66         }
67 
68         /** {@inheritDoc} */
onServiceDisconnected(ComponentName name)69         @Override public void onServiceDisconnected(ComponentName name) {
70             Log.d(this, "onDisconnected: %s", name);
71             onDisconnected(name);
72         }
73     }
74 
75     private final Call.Listener mCallListener = new Call.ListenerBase() {
76         @Override
77         public void onCallCapabilitiesChanged(Call call) {
78             updateCall(call);
79         }
80 
81         @Override
82         public void onCannedSmsResponsesLoaded(Call call) {
83             updateCall(call);
84         }
85 
86         @Override
87         public void onVideoCallProviderChanged(Call call) {
88             updateCall(call);
89         }
90 
91         @Override
92         public void onStatusHintsChanged(Call call) {
93             updateCall(call);
94         }
95 
96         @Override
97         public void onHandleChanged(Call call) {
98             updateCall(call);
99         }
100 
101         @Override
102         public void onCallerDisplayNameChanged(Call call) {
103             updateCall(call);
104         }
105 
106         @Override
107         public void onVideoStateChanged(Call call) {
108             updateCall(call);
109         }
110 
111         @Override
112         public void onTargetPhoneAccountChanged(Call call) {
113             updateCall(call);
114         }
115 
116         @Override
117         public void onConferenceableCallsChanged(Call call) {
118             updateCall(call);
119         }
120     };
121 
122     /**
123      * Maintains a binding connection to the in-call app(s).
124      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
125      * load factor before resizing, 1 means we only expect a single thread to
126      * access the map so make only a single shard
127      */
128     private final Map<ComponentName, InCallServiceConnection> mServiceConnections =
129             new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1);
130 
131     /** The in-call app implementations, see {@link IInCallService}. */
132     private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
133 
134     private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall");
135 
136     /** The {@link ComponentName} of the default InCall UI. */
137     private final ComponentName mInCallComponentName;
138 
139     private final Context mContext;
140 
InCallController(Context context)141     public InCallController(Context context) {
142         mContext = context;
143         Resources resources = mContext.getResources();
144 
145         mInCallComponentName = new ComponentName(
146                 resources.getString(R.string.ui_default_package),
147                 resources.getString(R.string.incall_default_class));
148     }
149 
150     @Override
onCallAdded(Call call)151     public void onCallAdded(Call call) {
152         if (mInCallServices.isEmpty()) {
153             bind();
154         } else {
155             Log.i(this, "onCallAdded: %s", call);
156             // Track the call if we don't already know about it.
157             addCall(call);
158 
159             for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
160                 ComponentName componentName = entry.getKey();
161                 IInCallService inCallService = entry.getValue();
162 
163                 ParcelableCall parcelableCall = toParcelableCall(call,
164                         componentName.equals(mInCallComponentName) /* includeVideoProvider */);
165                 try {
166                     inCallService.addCall(parcelableCall);
167                 } catch (RemoteException ignored) {
168                 }
169             }
170         }
171     }
172 
173     @Override
onCallRemoved(Call call)174     public void onCallRemoved(Call call) {
175         Log.i(this, "onCallRemoved: %s", call);
176         if (CallsManager.getInstance().getCalls().isEmpty()) {
177             // TODO: Wait for all messages to be delivered to the service before unbinding.
178             unbind();
179         }
180         call.removeListener(mCallListener);
181         mCallIdMapper.removeCall(call);
182     }
183 
184     @Override
onCallStateChanged(Call call, int oldState, int newState)185     public void onCallStateChanged(Call call, int oldState, int newState) {
186         updateCall(call);
187     }
188 
189     @Override
onConnectionServiceChanged( Call call, ConnectionServiceWrapper oldService, ConnectionServiceWrapper newService)190     public void onConnectionServiceChanged(
191             Call call,
192             ConnectionServiceWrapper oldService,
193             ConnectionServiceWrapper newService) {
194         updateCall(call);
195     }
196 
197     @Override
onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState)198     public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
199         if (!mInCallServices.isEmpty()) {
200             Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
201                     newAudioState);
202             for (IInCallService inCallService : mInCallServices.values()) {
203                 try {
204                     inCallService.onAudioStateChanged(newAudioState);
205                 } catch (RemoteException ignored) {
206                 }
207             }
208         }
209     }
210 
onPostDialWait(Call call, String remaining)211     void onPostDialWait(Call call, String remaining) {
212         if (!mInCallServices.isEmpty()) {
213             Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
214             for (IInCallService inCallService : mInCallServices.values()) {
215                 try {
216                     inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
217                 } catch (RemoteException ignored) {
218                 }
219             }
220         }
221     }
222 
223     @Override
onIsConferencedChanged(Call call)224     public void onIsConferencedChanged(Call call) {
225         Log.d(this, "onIsConferencedChanged %s", call);
226         updateCall(call);
227     }
228 
bringToForeground(boolean showDialpad)229     void bringToForeground(boolean showDialpad) {
230         if (!mInCallServices.isEmpty()) {
231             for (IInCallService inCallService : mInCallServices.values()) {
232                 try {
233                     inCallService.bringToForeground(showDialpad);
234                 } catch (RemoteException ignored) {
235                 }
236             }
237         } else {
238             Log.w(this, "Asking to bring unbound in-call UI to foreground.");
239         }
240     }
241 
242     /**
243      * Unbinds an existing bound connection to the in-call app.
244      */
unbind()245     private void unbind() {
246         ThreadUtil.checkOnMainThread();
247         Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator =
248             mServiceConnections.entrySet().iterator();
249         while (iterator.hasNext()) {
250             Log.i(this, "Unbinding from InCallService %s");
251             mContext.unbindService(iterator.next().getValue());
252             iterator.remove();
253         }
254         mInCallServices.clear();
255     }
256 
257     /**
258      * Binds to the in-call app if not already connected by binding directly to the saved
259      * component name of the {@link IInCallService} implementation.
260      */
bind()261     private void bind() {
262         ThreadUtil.checkOnMainThread();
263         if (mInCallServices.isEmpty()) {
264             PackageManager packageManager = mContext.getPackageManager();
265             Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
266 
267             for (ResolveInfo entry : packageManager.queryIntentServices(serviceIntent, 0)) {
268                 ServiceInfo serviceInfo = entry.serviceInfo;
269                 if (serviceInfo != null) {
270                     boolean hasServiceBindPermission = serviceInfo.permission != null &&
271                             serviceInfo.permission.equals(
272                                     Manifest.permission.BIND_INCALL_SERVICE);
273                     boolean hasControlInCallPermission = packageManager.checkPermission(
274                             Manifest.permission.CONTROL_INCALL_EXPERIENCE,
275                             serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
276 
277                     if (!hasServiceBindPermission) {
278                         Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " +
279                                 serviceInfo.packageName);
280                         continue;
281                     }
282 
283                     if (!hasControlInCallPermission) {
284                         Log.w(this,
285                                 "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " +
286                                         serviceInfo.packageName);
287                         continue;
288                     }
289 
290                     InCallServiceConnection inCallServiceConnection = new InCallServiceConnection();
291                     ComponentName componentName = new ComponentName(serviceInfo.packageName,
292                             serviceInfo.name);
293 
294                     Log.i(this, "Attempting to bind to InCall %s, is dupe? %b ",
295                             serviceInfo.packageName,
296                             mServiceConnections.containsKey(componentName));
297 
298                     if (!mServiceConnections.containsKey(componentName)) {
299                         Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
300                         intent.setComponent(componentName);
301 
302                         if (mContext.bindServiceAsUser(intent, inCallServiceConnection,
303                                 Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
304                             mServiceConnections.put(componentName, inCallServiceConnection);
305                         }
306                     }
307                 }
308             }
309         }
310     }
311 
312     /**
313      * Persists the {@link IInCallService} instance and starts the communication between
314      * this class and in-call app by sending the first update to in-call app. This method is
315      * called after a successful binding connection is established.
316      *
317      * @param componentName The service {@link ComponentName}.
318      * @param service The {@link IInCallService} implementation.
319      */
onConnected(ComponentName componentName, IBinder service)320     private void onConnected(ComponentName componentName, IBinder service) {
321         ThreadUtil.checkOnMainThread();
322 
323         Log.i(this, "onConnected to %s", componentName);
324 
325         IInCallService inCallService = IInCallService.Stub.asInterface(service);
326 
327         try {
328             inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(),
329                     mCallIdMapper));
330             mInCallServices.put(componentName, inCallService);
331         } catch (RemoteException e) {
332             Log.e(this, e, "Failed to set the in-call adapter.");
333             return;
334         }
335 
336         // Upon successful connection, send the state of the world to the service.
337         ImmutableCollection<Call> calls = CallsManager.getInstance().getCalls();
338         if (!calls.isEmpty()) {
339             Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
340                     componentName);
341             for (Call call : calls) {
342                 try {
343                     // Track the call if we don't already know about it.
344                     Log.i(this, "addCall after binding: %s", call);
345                     addCall(call);
346 
347                     inCallService.addCall(toParcelableCall(call,
348                             componentName.equals(mInCallComponentName) /* includeVideoProvider */));
349                 } catch (RemoteException ignored) {
350                 }
351             }
352             onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
353         } else {
354             unbind();
355         }
356     }
357 
358     /**
359      * Cleans up an instance of in-call app after the service has been unbound.
360      *
361      * @param disconnectedComponent The {@link ComponentName} of the service which disconnected.
362      */
onDisconnected(ComponentName disconnectedComponent)363     private void onDisconnected(ComponentName disconnectedComponent) {
364         Log.i(this, "onDisconnected from %s", disconnectedComponent);
365         ThreadUtil.checkOnMainThread();
366 
367         if (mInCallServices.containsKey(disconnectedComponent)) {
368             mInCallServices.remove(disconnectedComponent);
369         }
370 
371         if (mServiceConnections.containsKey(disconnectedComponent)) {
372             // One of the services that we were bound to has disconnected. If the default in-call UI
373             // has disconnected, disconnect all calls and un-bind all other InCallService
374             // implementations.
375             if (disconnectedComponent.equals(mInCallComponentName)) {
376                 Log.i(this, "In-call UI %s disconnected.", disconnectedComponent);
377                 CallsManager.getInstance().disconnectAllCalls();
378                 unbind();
379             } else {
380                 Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent);
381                 // Else, if it wasn't the default in-call UI, then one of the other in-call services
382                 // disconnected and, well, that's probably their fault.  Clear their state and
383                 // ignore.
384                 InCallServiceConnection serviceConnection =
385                         mServiceConnections.get(disconnectedComponent);
386 
387                 // We still need to call unbind even though it disconnected.
388                 mContext.unbindService(serviceConnection);
389 
390                 mServiceConnections.remove(disconnectedComponent);
391                 mInCallServices.remove(disconnectedComponent);
392             }
393         }
394     }
395 
396     /**
397      * Informs all {@link InCallService} instances of the updated call information.  Changes to the
398      * video provider are only communicated to the default in-call UI.
399      *
400      * @param call The {@link Call}.
401      */
updateCall(Call call)402     private void updateCall(Call call) {
403         if (!mInCallServices.isEmpty()) {
404             for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
405                 ComponentName componentName = entry.getKey();
406                 IInCallService inCallService = entry.getValue();
407                 ParcelableCall parcelableCall = toParcelableCall(call,
408                         componentName.equals(mInCallComponentName) /* includeVideoProvider */);
409 
410                 Log.v(this, "updateCall %s ==> %s", call, parcelableCall);
411                 try {
412                     inCallService.updateCall(parcelableCall);
413                 } catch (RemoteException ignored) {
414                 }
415             }
416         }
417     }
418 
419     /**
420      * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
421      *
422      * @param call The {@link Call} to parcel.
423      * @param includeVideoProvider When {@code true}, the {@link IVideoProvider} is included in the
424      *      parcelled call.  When {@code false}, the {@link IVideoProvider} is not included.
425      * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
426      */
toParcelableCall(Call call, boolean includeVideoProvider)427     private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) {
428         String callId = mCallIdMapper.getCallId(call);
429 
430         int capabilities = call.getCallCapabilities();
431         if (CallsManager.getInstance().isAddCallCapable(call)) {
432             capabilities |= PhoneCapabilities.ADD_CALL;
433         }
434 
435         // Disable mute and add call for emergency calls.
436         if (call.isEmergencyCall()) {
437             capabilities &= ~PhoneCapabilities.MUTE;
438             capabilities &= ~PhoneCapabilities.ADD_CALL;
439         }
440 
441         int properties = call.isConference() ? CallProperties.CONFERENCE : 0;
442 
443         int state = call.getState();
444         if (state == CallState.ABORTED) {
445             state = CallState.DISCONNECTED;
446         }
447 
448         if (call.isLocallyDisconnecting() && state != CallState.DISCONNECTED) {
449             state = CallState.DISCONNECTING;
450         }
451 
452         String parentCallId = null;
453         Call parentCall = call.getParentCall();
454         if (parentCall != null) {
455             parentCallId = mCallIdMapper.getCallId(parentCall);
456         }
457 
458         long connectTimeMillis = call.getConnectTimeMillis();
459         List<Call> childCalls = call.getChildCalls();
460         List<String> childCallIds = new ArrayList<>();
461         if (!childCalls.isEmpty()) {
462             connectTimeMillis = Long.MAX_VALUE;
463             for (Call child : childCalls) {
464                 if (child.getConnectTimeMillis() > 0) {
465                     connectTimeMillis = Math.min(child.getConnectTimeMillis(), connectTimeMillis);
466                 }
467                 childCallIds.add(mCallIdMapper.getCallId(child));
468             }
469         }
470 
471         if (call.isRespondViaSmsCapable()) {
472             capabilities |= PhoneCapabilities.RESPOND_VIA_TEXT;
473         }
474 
475         Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
476                 call.getHandle() : null;
477         String callerDisplayName = call.getCallerDisplayNamePresentation() ==
478                 TelecomManager.PRESENTATION_ALLOWED ?  call.getCallerDisplayName() : null;
479 
480         List<Call> conferenceableCalls = call.getConferenceableCalls();
481         List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
482         for (Call otherCall : conferenceableCalls) {
483             String otherId = mCallIdMapper.getCallId(otherCall);
484             if (otherId != null) {
485                 conferenceableCallIds.add(otherId);
486             }
487         }
488 
489         return new ParcelableCall(
490                 callId,
491                 state,
492                 call.getDisconnectCause(),
493                 call.getCannedSmsResponses(),
494                 capabilities,
495                 properties,
496                 connectTimeMillis,
497                 handle,
498                 call.getHandlePresentation(),
499                 callerDisplayName,
500                 call.getCallerDisplayNamePresentation(),
501                 call.getGatewayInfo(),
502                 call.getTargetPhoneAccount(),
503                 includeVideoProvider ? call.getVideoProvider() : null,
504                 parentCallId,
505                 childCallIds,
506                 call.getStatusHints(),
507                 call.getVideoState(),
508                 conferenceableCallIds,
509                 call.getExtras());
510     }
511 
512     /**
513      * Adds the call to the list of calls tracked by the {@link InCallController}.
514      * @param call The call to add.
515      */
addCall(Call call)516     private void addCall(Call call) {
517         if (mCallIdMapper.getCallId(call) == null) {
518             mCallIdMapper.addCall(call);
519             call.addListener(mCallListener);
520         }
521     }
522 }
523