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