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