InCallController.java revision f78a72f8a1054726aa6f85a21372d917576906dd
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.Handler;
30import android.os.IBinder;
31import android.os.Looper;
32import android.os.RemoteException;
33import android.os.Trace;
34import android.os.UserHandle;
35import android.telecom.Call.Details;
36import android.telecom.CallAudioState;
37import android.telecom.Connection;
38import android.telecom.DefaultDialerManager;
39import android.telecom.InCallService;
40import android.telecom.ParcelableCall;
41import android.telecom.PhoneAccount;
42import android.telecom.PhoneAccountHandle;
43import android.telecom.TelecomManager;
44import android.telecom.VideoCallImpl;
45import android.util.ArrayMap;
46
47// TODO: Needed for move to system service: import com.android.internal.R;
48import com.android.internal.telecom.IInCallService;
49import com.android.internal.util.IndentingPrintWriter;
50import com.android.server.telecom.SystemStateProvider.SystemStateListener;
51
52import java.util.ArrayList;
53import java.util.Collection;
54import java.util.Iterator;
55import java.util.LinkedList;
56import java.util.List;
57import java.util.Map;
58import java.util.Objects;
59import java.util.concurrent.ConcurrentHashMap;
60
61/**
62 * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
63 * can send updates to the in-call app. This class is created and owned by CallsManager and retains
64 * a binding to the {@link IInCallService} (implemented by the in-call app).
65 */
66public final class InCallController extends CallsManagerListenerBase {
67    /**
68     * Used to bind to the in-call app and triggers the start of communication between
69     * this class and in-call app.
70     */
71    private class InCallServiceConnection implements ServiceConnection {
72        /** {@inheritDoc} */
73        @Override public void onServiceConnected(ComponentName name, IBinder service) {
74            Log.startSession("ICSC.oSC");
75            Log.d(this, "onServiceConnected: %s", name);
76            onConnected(name, service);
77            Log.endSession();
78        }
79
80        /** {@inheritDoc} */
81        @Override public void onServiceDisconnected(ComponentName name) {
82            Log.startSession("ICSC.oSD");
83            Log.d(this, "onDisconnected: %s", name);
84            onDisconnected(name);
85            Log.endSession();
86        }
87    }
88
89    private final Call.Listener mCallListener = new Call.ListenerBase() {
90        @Override
91        public void onConnectionCapabilitiesChanged(Call call) {
92            updateCall(call);
93        }
94
95        @Override
96        public void onCannedSmsResponsesLoaded(Call call) {
97            updateCall(call);
98        }
99
100        @Override
101        public void onVideoCallProviderChanged(Call call) {
102            updateCall(call, true /* videoProviderChanged */);
103        }
104
105        @Override
106        public void onStatusHintsChanged(Call call) {
107            updateCall(call);
108        }
109
110        @Override
111        public void onExtrasChanged(Call call) {
112            updateCall(call);
113        }
114
115        @Override
116        public void onHandleChanged(Call call) {
117            updateCall(call);
118        }
119
120        @Override
121        public void onCallerDisplayNameChanged(Call call) {
122            updateCall(call);
123        }
124
125        @Override
126        public void onVideoStateChanged(Call call) {
127            updateCall(call);
128        }
129
130        @Override
131        public void onTargetPhoneAccountChanged(Call call) {
132            updateCall(call);
133        }
134
135        @Override
136        public void onConferenceableCallsChanged(Call call) {
137            updateCall(call);
138        }
139    };
140
141    private final SystemStateListener mSystemStateListener = new SystemStateListener() {
142        @Override
143        public void onCarModeChanged(boolean isCarMode) {
144            // Do something when the car mode changes.
145        }
146    };
147
148    private static final int IN_CALL_SERVICE_TYPE_INVALID = 0;
149    private static final int IN_CALL_SERVICE_TYPE_DIALER_UI = 1;
150    private static final int IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
151    private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
152    private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
153
154    /**
155     * Maintains a binding connection to the in-call app(s).
156     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
157     * load factor before resizing, 1 means we only expect a single thread to
158     * access the map so make only a single shard
159     */
160    private final Map<ComponentName, InCallServiceConnection> mServiceConnections =
161            new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1);
162
163    /** The in-call app implementations, see {@link IInCallService}. */
164    private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
165
166    /**
167     * The {@link ComponentName} of the bound In-Call UI Service.
168     */
169    private ComponentName mInCallUIComponentName;
170
171    private final CallIdMapper mCallIdMapper = new CallIdMapper();
172
173    /** The {@link ComponentName} of the default InCall UI. */
174    private final ComponentName mSystemInCallComponentName;
175
176    private final Context mContext;
177    private final TelecomSystem.SyncRoot mLock;
178    private final CallsManager mCallsManager;
179    private final SystemStateProvider mSystemStateProvider;
180
181    public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
182            SystemStateProvider systemStateProvider) {
183        mContext = context;
184        mLock = lock;
185        mCallsManager = callsManager;
186        mSystemStateProvider = systemStateProvider;
187
188        Resources resources = mContext.getResources();
189        mSystemInCallComponentName = new ComponentName(
190                resources.getString(R.string.ui_default_package),
191                resources.getString(R.string.incall_default_class));
192
193        mSystemStateProvider.addListener(mSystemStateListener);
194    }
195
196    @Override
197    public void onCallAdded(Call call) {
198        if (!isBoundToServices()) {
199            bindToServices(call);
200        } else {
201            adjustServiceBindingsForEmergency();
202
203            Log.i(this, "onCallAdded: %s", call);
204            // Track the call if we don't already know about it.
205            addCall(call);
206
207            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
208                ComponentName componentName = entry.getKey();
209                IInCallService inCallService = entry.getValue();
210                ParcelableCall parcelableCall = toParcelableCall(call,
211                        true /* includeVideoProvider */);
212                try {
213                    inCallService.addCall(parcelableCall);
214                } catch (RemoteException ignored) {
215                }
216            }
217        }
218    }
219
220    @Override
221    public void onCallRemoved(Call call) {
222        Log.i(this, "onCallRemoved: %s", call);
223        if (mCallsManager.getCalls().isEmpty()) {
224            /** Let's add a 2 second delay before we send unbind to the services to hopefully
225             *  give them enough time to process all the pending messages.
226             */
227            final Session subsession = Log.createSubsession();
228            Handler handler = new Handler(Looper.getMainLooper());
229            final Runnable runnableUnbind = new Runnable() {
230                @Override
231                public void run() {
232                    try {
233                        Log.continueSession(subsession, "ICC.oCR");
234                        synchronized (mLock) {
235                            // Check again to make sure there are no active calls.
236                            if (mCallsManager.getCalls().isEmpty()) {
237                                unbindFromServices();
238                            }
239                        }
240                    } finally {
241                        Log.endSession();
242                    }
243                }
244            };
245            handler.postDelayed(
246                    runnableUnbind,
247                    Timeouts.getCallRemoveUnbindInCallServicesDelay(
248                            mContext.getContentResolver()));
249        }
250        call.removeListener(mCallListener);
251        mCallIdMapper.removeCall(call);
252    }
253
254    @Override
255    public void onCallStateChanged(Call call, int oldState, int newState) {
256        updateCall(call);
257    }
258
259    @Override
260    public void onConnectionServiceChanged(
261            Call call,
262            ConnectionServiceWrapper oldService,
263            ConnectionServiceWrapper newService) {
264        updateCall(call);
265    }
266
267    @Override
268    public void onCallAudioStateChanged(CallAudioState oldCallAudioState,
269            CallAudioState newCallAudioState) {
270        if (!mInCallServices.isEmpty()) {
271            Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
272                    newCallAudioState);
273            for (IInCallService inCallService : mInCallServices.values()) {
274                try {
275                    inCallService.onCallAudioStateChanged(newCallAudioState);
276                } catch (RemoteException ignored) {
277                }
278            }
279        }
280    }
281
282    @Override
283    public void onCanAddCallChanged(boolean canAddCall) {
284        if (!mInCallServices.isEmpty()) {
285            Log.i(this, "onCanAddCallChanged : %b", canAddCall);
286            for (IInCallService inCallService : mInCallServices.values()) {
287                try {
288                    inCallService.onCanAddCallChanged(canAddCall);
289                } catch (RemoteException ignored) {
290                }
291            }
292        }
293    }
294
295    void onPostDialWait(Call call, String remaining) {
296        if (!mInCallServices.isEmpty()) {
297            Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
298            for (IInCallService inCallService : mInCallServices.values()) {
299                try {
300                    inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
301                } catch (RemoteException ignored) {
302                }
303            }
304        }
305    }
306
307    @Override
308    public void onIsConferencedChanged(Call call) {
309        Log.d(this, "onIsConferencedChanged %s", call);
310        updateCall(call);
311    }
312
313    void bringToForeground(boolean showDialpad) {
314        if (!mInCallServices.isEmpty()) {
315            for (IInCallService inCallService : mInCallServices.values()) {
316                try {
317                    inCallService.bringToForeground(showDialpad);
318                } catch (RemoteException ignored) {
319                }
320            }
321        } else {
322            Log.w(this, "Asking to bring unbound in-call UI to foreground.");
323        }
324    }
325
326    /**
327     * Unbinds an existing bound connection to the in-call app.
328     */
329    private void unbindFromServices() {
330        Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator =
331            mServiceConnections.entrySet().iterator();
332        while (iterator.hasNext()) {
333            final Map.Entry<ComponentName, InCallServiceConnection> entry = iterator.next();
334            Log.i(this, "Unbinding from InCallService %s", entry.getKey());
335            try {
336                mContext.unbindService(entry.getValue());
337            } catch (Exception e) {
338                Log.e(this, e, "Exception while unbinding from InCallService");
339            }
340            iterator.remove();
341        }
342        mInCallServices.clear();
343    }
344
345    /**
346     * Binds to all the UI-providing InCallService as well as system-implemented non-UI
347     * InCallServices. Method-invoker must check {@link #isBoundToServices()} before invoking.
348     *
349     * @param call The newly added call that triggered the binding to the in-call services.
350     */
351    private void bindToServices(Call call) {
352        ComponentName inCallUIService = null;
353        ComponentName carModeInCallUIService = null;
354        List<ComponentName> nonUIInCallServices = new LinkedList<>();
355
356        // Loop through all the InCallService implementations that exist in the devices;
357        PackageManager packageManager = mContext.getPackageManager();
358        Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
359        for (ResolveInfo entry :
360                packageManager.queryIntentServices(serviceIntent, PackageManager.GET_META_DATA)) {
361            ServiceInfo serviceInfo = entry.serviceInfo;
362            if (serviceInfo != null) {
363                ComponentName componentName =
364                        new ComponentName(serviceInfo.packageName, serviceInfo.name);
365
366                switch (getInCallServiceType(entry.serviceInfo, packageManager)) {
367                    case IN_CALL_SERVICE_TYPE_DIALER_UI:
368                        if (inCallUIService == null ||
369                                inCallUIService.compareTo(componentName) > 0) {
370                            inCallUIService = componentName;
371                        }
372                        break;
373
374                    case IN_CALL_SERVICE_TYPE_SYSTEM_UI:
375                        // skip, will be added manually
376                        break;
377
378                    case IN_CALL_SERVICE_TYPE_CAR_MODE_UI:
379                        if (carModeInCallUIService == null ||
380                                carModeInCallUIService.compareTo(componentName) > 0) {
381                            carModeInCallUIService = componentName;
382                        }
383                        break;
384
385                    case IN_CALL_SERVICE_TYPE_NON_UI:
386                        nonUIInCallServices.add(componentName);
387                        break;
388
389                    case IN_CALL_SERVICE_TYPE_INVALID:
390                        break;
391
392                    default:
393                        Log.w(this, "unexpected in-call service type");
394                        break;
395                }
396            }
397        }
398
399        Log.i(this, "Car mode InCallService: %s", carModeInCallUIService);
400        Log.i(this, "Dialer InCallService: %s", inCallUIService);
401
402        // Adding the in-call services in order:
403        // (1) The carmode in-call if carmode is on.
404        // (2) The default-dialer in-call if not an emergency call
405        // (3) The system-provided in-call
406        List<ComponentName> orderedInCallUIServices = new LinkedList<>();
407        if (shouldUseCarModeUI() && carModeInCallUIService != null) {
408            orderedInCallUIServices.add(carModeInCallUIService);
409        }
410        if (!mCallsManager.hasEmergencyCall() && inCallUIService != null) {
411            orderedInCallUIServices.add(inCallUIService);
412        }
413        orderedInCallUIServices.add(mSystemInCallComponentName);
414
415        // TODO: Need to implement the fall-back logic in case the main UI in-call service rejects
416        // the binding request.
417        ComponentName inCallUIServiceToBind = orderedInCallUIServices.get(0);
418        if (!bindToInCallService(inCallUIServiceToBind, call, "ui")) {
419            Log.event(call, Log.Events.ERROR_LOG,
420                    "InCallService system UI failed binding: " + inCallUIService);
421        }
422        mInCallUIComponentName = inCallUIService;
423
424        // Bind to the control InCallServices
425        for (ComponentName componentName : nonUIInCallServices) {
426            bindToInCallService(componentName, call, "control");
427        }
428    }
429
430    /**
431     * Binds to the specified InCallService.
432     */
433    private boolean bindToInCallService(ComponentName componentName, Call call, String tag) {
434        if (mInCallServices.containsKey(componentName)) {
435            Log.i(this, "An InCallService already exists: %s", componentName);
436            return true;
437        }
438
439        if (mServiceConnections.containsKey(componentName)) {
440            Log.w(this, "The service is already bound for this component %s", componentName);
441            return true;
442        }
443
444        Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
445        intent.setComponent(componentName);
446        if (call != null && !call.isIncoming()){
447            intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,
448                    call.getIntentExtras());
449            intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
450                    call.getTargetPhoneAccount());
451        }
452
453        Log.i(this, "Attempting to bind to [%s] InCall %s, with %s", tag, componentName, intent);
454        InCallServiceConnection inCallServiceConnection = new InCallServiceConnection();
455        if (mContext.bindServiceAsUser(intent, inCallServiceConnection,
456                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
457                    UserHandle.CURRENT)) {
458            mServiceConnections.put(componentName, inCallServiceConnection);
459            return true;
460        }
461
462        return false;
463    }
464
465    private boolean shouldUseCarModeUI() {
466        return mSystemStateProvider.isCarMode();
467    }
468
469    /**
470     * Returns the type of InCallService described by the specified serviceInfo.
471     */
472    private int getInCallServiceType(ServiceInfo serviceInfo, PackageManager packageManager) {
473        // Verify that the InCallService requires the BIND_INCALL_SERVICE permission which
474        // enforces that only Telecom can bind to it.
475        boolean hasServiceBindPermission = serviceInfo.permission != null &&
476                serviceInfo.permission.equals(
477                        Manifest.permission.BIND_INCALL_SERVICE);
478        if (!hasServiceBindPermission) {
479            Log.w(this, "InCallService does not require BIND_INCALL_SERVICE permission: " +
480                    serviceInfo.packageName);
481            return IN_CALL_SERVICE_TYPE_INVALID;
482        }
483
484        if (mSystemInCallComponentName.getPackageName().equals(serviceInfo.packageName) &&
485                mSystemInCallComponentName.getClassName().equals(serviceInfo.name)) {
486            return IN_CALL_SERVICE_TYPE_SYSTEM_UI;
487        }
488
489        // Check to see if the service is a car-mode UI type by checking that it has the
490        // CONTROL_INCALL_EXPERIENCE (to verify it is a system app) and that it has the
491        // car-mode UI metadata.
492        boolean hasControlInCallPermission = packageManager.checkPermission(
493                Manifest.permission.CONTROL_INCALL_EXPERIENCE,
494                serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
495        boolean isCarModeUIService = serviceInfo.metaData != null &&
496                serviceInfo.metaData.getBoolean(
497                        TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false) &&
498                hasControlInCallPermission;
499        if (isCarModeUIService) {
500            return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
501        }
502
503
504        // Check to see that it is the default dialer package
505        boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
506                DefaultDialerManager.getDefaultDialerApplication(mContext));
507        boolean isUIService = serviceInfo.metaData != null &&
508                serviceInfo.metaData.getBoolean(
509                        TelecomManager.METADATA_IN_CALL_SERVICE_UI, false);
510        if (isDefaultDialerPackage && isUIService) {
511            return IN_CALL_SERVICE_TYPE_DIALER_UI;
512        }
513
514        // Also allow any in-call service that has the control-experience permission (to ensure
515        // that it is a system app) and doesn't claim to show any UI.
516        if (hasControlInCallPermission && !isUIService) {
517            return IN_CALL_SERVICE_TYPE_NON_UI;
518        }
519
520        // Anything else that remains, we will not bind to.
521        Log.i(this, "Skipping binding to %s:%s, control: %b, car-mode: %b, ui: %b",
522                serviceInfo.packageName, serviceInfo.name, hasControlInCallPermission,
523                isCarModeUIService, isUIService);
524        return IN_CALL_SERVICE_TYPE_INVALID;
525    }
526
527    private void adjustServiceBindingsForEmergency() {
528        if (!Objects.equals(mInCallUIComponentName, mSystemInCallComponentName)) {
529            // The connected UI is not the system UI, so lets check if we should switch them
530            // if there exists an emergency number.
531            if (mCallsManager.hasEmergencyCall()) {
532                // Lets fake a failure here in order to trigger the switch to the system UI.
533                onInCallServiceFailure(mInCallUIComponentName, "emergency adjust");
534            }
535        }
536    }
537
538    /**
539     * Persists the {@link IInCallService} instance and starts the communication between
540     * this class and in-call app by sending the first update to in-call app. This method is
541     * called after a successful binding connection is established.
542     *
543     * @param componentName The service {@link ComponentName}.
544     * @param service The {@link IInCallService} implementation.
545     */
546    private void onConnected(ComponentName componentName, IBinder service) {
547        Trace.beginSection("onConnected: " + componentName);
548        Log.i(this, "onConnected to %s", componentName);
549
550        IInCallService inCallService = IInCallService.Stub.asInterface(service);
551        mInCallServices.put(componentName, inCallService);
552
553        try {
554            inCallService.setInCallAdapter(
555                    new InCallAdapter(
556                            mCallsManager,
557                            mCallIdMapper,
558                            mLock,
559                            componentName.getPackageName()));
560        } catch (RemoteException e) {
561            Log.e(this, e, "Failed to set the in-call adapter.");
562            Trace.endSection();
563            onInCallServiceFailure(componentName, "setInCallAdapter");
564            return;
565        }
566
567        // Upon successful connection, send the state of the world to the service.
568        Collection<Call> calls = mCallsManager.getCalls();
569        if (!calls.isEmpty()) {
570            Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
571                    componentName);
572            for (Call call : calls) {
573                try {
574                    // Track the call if we don't already know about it.
575                    addCall(call);
576                    inCallService.addCall(toParcelableCall(call, true /* includeVideoProvider */));
577                } catch (RemoteException ignored) {
578                }
579            }
580            onCallAudioStateChanged(
581                    null,
582                    mCallsManager.getAudioState());
583            onCanAddCallChanged(mCallsManager.canAddCall());
584        } else {
585            unbindFromServices();
586        }
587        Trace.endSection();
588    }
589
590    /**
591     * Cleans up an instance of in-call app after the service has been unbound.
592     *
593     * @param disconnectedComponent The {@link ComponentName} of the service which disconnected.
594     */
595    private void onDisconnected(ComponentName disconnectedComponent) {
596        Log.i(this, "onDisconnected from %s", disconnectedComponent);
597
598        mInCallServices.remove(disconnectedComponent);
599        if (mServiceConnections.containsKey(disconnectedComponent)) {
600            // One of the services that we were bound to has unexpectedly disconnected.
601            onInCallServiceFailure(disconnectedComponent, "onDisconnect");
602        }
603    }
604
605    /**
606     * Handles non-recoverable failures by the InCallService. This method performs cleanup and
607     * special handling when the failure is to the UI InCallService.
608     */
609    private void onInCallServiceFailure(ComponentName componentName, String tag) {
610        Log.i(this, "Cleaning up a failed InCallService [%s]: %s", tag, componentName);
611
612        // We always clean up the connections here. Even in the case where we rebind to the UI
613        // because binding is count based and we could end up double-bound.
614        mInCallServices.remove(componentName);
615        InCallServiceConnection serviceConnection = mServiceConnections.remove(componentName);
616        if (serviceConnection != null) {
617            // We still need to call unbind even though it disconnected.
618            mContext.unbindService(serviceConnection);
619        }
620
621        if (Objects.equals(mInCallUIComponentName, componentName)) {
622            if (!mCallsManager.hasAnyCalls()) {
623                // No calls are left anyway. Lets just disconnect all of them.
624                unbindFromServices();
625                return;
626            }
627
628            // Whenever the UI crashes, we automatically revert to the System UI for the
629            // remainder of the active calls.
630            mInCallUIComponentName = mSystemInCallComponentName;
631            bindToInCallService(mInCallUIComponentName, null, "reconnecting");
632        }
633    }
634
635    /**
636     * Informs all {@link InCallService} instances of the updated call information.
637     *
638     * @param call The {@link Call}.
639     */
640    private void updateCall(Call call) {
641        updateCall(call, false /* videoProviderChanged */);
642    }
643
644    /**
645     * Informs all {@link InCallService} instances of the updated call information.
646     *
647     * @param call The {@link Call}.
648     * @param videoProviderChanged {@code true} if the video provider changed, {@code false}
649     *      otherwise.
650     */
651    private void updateCall(Call call, boolean videoProviderChanged) {
652        if (!mInCallServices.isEmpty()) {
653            ParcelableCall parcelableCall = toParcelableCall(call,
654                    videoProviderChanged /* includeVideoProvider */);
655            Log.i(this, "Sending updateCall %s ==> %s", call, parcelableCall);
656            List<ComponentName> componentsUpdated = new ArrayList<>();
657            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
658                ComponentName componentName = entry.getKey();
659                IInCallService inCallService = entry.getValue();
660                componentsUpdated.add(componentName);
661                try {
662                    inCallService.updateCall(parcelableCall);
663                } catch (RemoteException ignored) {
664                }
665            }
666            Log.i(this, "Components updated: %s", componentsUpdated);
667        }
668    }
669
670    /**
671     * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
672     *
673     * @param call The {@link Call} to parcel.
674     * @param includeVideoProvider {@code true} if the video provider should be parcelled with the
675     *      {@link Call}, {@code false} otherwise.  Since the {@link ParcelableCall#getVideoCall()}
676     *      method creates a {@link VideoCallImpl} instance on access it is important for the
677     *      recipient of the {@link ParcelableCall} to know if the video provider changed.
678     * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
679     */
680    private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) {
681        String callId = mCallIdMapper.getCallId(call);
682
683        int state = getParcelableState(call);
684        int capabilities = convertConnectionToCallCapabilities(call.getConnectionCapabilities());
685        int properties = convertConnectionToCallProperties(call.getConnectionCapabilities());
686        if (call.isConference()) {
687            properties |= android.telecom.Call.Details.PROPERTY_CONFERENCE;
688        }
689
690        final PhoneAccountRegistrar phoneAccountRegistrar =
691                mCallsManager.getPhoneAccountRegistrar();
692
693        if (call.isWorkCall(phoneAccountRegistrar)) {
694            properties |= android.telecom.Call.Details.PROPERTY_WORK_CALL;
695        }
696
697        // If this is a single-SIM device, the "default SIM" will always be the only SIM.
698        boolean isDefaultSmsAccount =
699                phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
700        if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) {
701            capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT;
702        }
703
704        if (call.isEmergencyCall()) {
705            capabilities = removeCapability(
706                    capabilities, android.telecom.Call.Details.CAPABILITY_MUTE);
707        }
708
709        if (state == android.telecom.Call.STATE_DIALING) {
710            capabilities = removeCapability(capabilities,
711                    android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
712            capabilities = removeCapability(capabilities,
713                    android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
714        }
715
716        String parentCallId = null;
717        Call parentCall = call.getParentCall();
718        if (parentCall != null) {
719            parentCallId = mCallIdMapper.getCallId(parentCall);
720        }
721
722        long connectTimeMillis = call.getConnectTimeMillis();
723        List<Call> childCalls = call.getChildCalls();
724        List<String> childCallIds = new ArrayList<>();
725        if (!childCalls.isEmpty()) {
726            long childConnectTimeMillis = Long.MAX_VALUE;
727            for (Call child : childCalls) {
728                if (child.getConnectTimeMillis() > 0) {
729                    childConnectTimeMillis = Math.min(child.getConnectTimeMillis(),
730                            childConnectTimeMillis);
731                }
732                childCallIds.add(mCallIdMapper.getCallId(child));
733            }
734
735            if (childConnectTimeMillis != Long.MAX_VALUE) {
736                connectTimeMillis = childConnectTimeMillis;
737            }
738        }
739
740        Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
741                call.getHandle() : null;
742        String callerDisplayName = call.getCallerDisplayNamePresentation() ==
743                TelecomManager.PRESENTATION_ALLOWED ?  call.getCallerDisplayName() : null;
744
745        List<Call> conferenceableCalls = call.getConferenceableCalls();
746        List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
747        for (Call otherCall : conferenceableCalls) {
748            String otherId = mCallIdMapper.getCallId(otherCall);
749            if (otherId != null) {
750                conferenceableCallIds.add(otherId);
751            }
752        }
753
754        return new ParcelableCall(
755                callId,
756                state,
757                call.getDisconnectCause(),
758                call.getCannedSmsResponses(),
759                capabilities,
760                properties,
761                connectTimeMillis,
762                handle,
763                call.getHandlePresentation(),
764                callerDisplayName,
765                call.getCallerDisplayNamePresentation(),
766                call.getGatewayInfo(),
767                call.getTargetPhoneAccount(),
768                includeVideoProvider,
769                includeVideoProvider ? call.getVideoProvider() : null,
770                parentCallId,
771                childCallIds,
772                call.getStatusHints(),
773                call.getVideoState(),
774                conferenceableCallIds,
775                call.getIntentExtras(),
776                call.getExtras());
777    }
778
779    private static int getParcelableState(Call call) {
780        int state = CallState.NEW;
781        switch (call.getState()) {
782            case CallState.ABORTED:
783            case CallState.DISCONNECTED:
784                state = android.telecom.Call.STATE_DISCONNECTED;
785                break;
786            case CallState.ACTIVE:
787                state = android.telecom.Call.STATE_ACTIVE;
788                break;
789            case CallState.CONNECTING:
790                state = android.telecom.Call.STATE_CONNECTING;
791                break;
792            case CallState.DIALING:
793                state = android.telecom.Call.STATE_DIALING;
794                break;
795            case CallState.DISCONNECTING:
796                state = android.telecom.Call.STATE_DISCONNECTING;
797                break;
798            case CallState.NEW:
799                state = android.telecom.Call.STATE_NEW;
800                break;
801            case CallState.ON_HOLD:
802                state = android.telecom.Call.STATE_HOLDING;
803                break;
804            case CallState.RINGING:
805                state = android.telecom.Call.STATE_RINGING;
806                break;
807            case CallState.SELECT_PHONE_ACCOUNT:
808                state = android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT;
809                break;
810        }
811
812        // If we are marked as 'locally disconnecting' then mark ourselves as disconnecting instead.
813        // Unless we're disconnect*ED*, in which case leave it at that.
814        if (call.isLocallyDisconnecting() &&
815                (state != android.telecom.Call.STATE_DISCONNECTED)) {
816            state = android.telecom.Call.STATE_DISCONNECTING;
817        }
818        return state;
819    }
820
821    private static final int[] CONNECTION_TO_CALL_CAPABILITY = new int[] {
822        Connection.CAPABILITY_HOLD,
823        android.telecom.Call.Details.CAPABILITY_HOLD,
824
825        Connection.CAPABILITY_SUPPORT_HOLD,
826        android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD,
827
828        Connection.CAPABILITY_MERGE_CONFERENCE,
829        android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE,
830
831        Connection.CAPABILITY_SWAP_CONFERENCE,
832        android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE,
833
834        Connection.CAPABILITY_RESPOND_VIA_TEXT,
835        android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT,
836
837        Connection.CAPABILITY_MUTE,
838        android.telecom.Call.Details.CAPABILITY_MUTE,
839
840        Connection.CAPABILITY_MANAGE_CONFERENCE,
841        android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE,
842
843        Connection.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
844        android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_RX,
845
846        Connection.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
847        android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX,
848
849        Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
850        android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL,
851
852        Connection.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
853        android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX,
854
855        Connection.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
856        android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_TX,
857
858        Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
859        android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL,
860
861        Connection.CAPABILITY_SEPARATE_FROM_CONFERENCE,
862        android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE,
863
864        Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
865        android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE,
866
867        Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
868        android.telecom.Call.Details.CAPABILITY_CAN_UPGRADE_TO_VIDEO,
869
870        Connection.CAPABILITY_CAN_PAUSE_VIDEO,
871        android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO,
872
873        Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION,
874        android.telecom.Call.Details.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
875    };
876
877    private static int convertConnectionToCallCapabilities(int connectionCapabilities) {
878        int callCapabilities = 0;
879        for (int i = 0; i < CONNECTION_TO_CALL_CAPABILITY.length; i += 2) {
880            if ((CONNECTION_TO_CALL_CAPABILITY[i] & connectionCapabilities) ==
881                    CONNECTION_TO_CALL_CAPABILITY[i]) {
882
883                callCapabilities |= CONNECTION_TO_CALL_CAPABILITY[i + 1];
884            }
885        }
886        return callCapabilities;
887    }
888
889    private static final int[] CONNECTION_TO_CALL_PROPERTIES = new int[] {
890        Connection.CAPABILITY_HIGH_DEF_AUDIO,
891        android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO,
892
893        Connection.CAPABILITY_WIFI,
894        android.telecom.Call.Details.PROPERTY_WIFI,
895
896        Connection.CAPABILITY_GENERIC_CONFERENCE,
897        android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE,
898
899        Connection.CAPABILITY_SHOW_CALLBACK_NUMBER,
900        android.telecom.Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE,
901    };
902
903    private static int convertConnectionToCallProperties(int connectionCapabilities) {
904        int callProperties = 0;
905        for (int i = 0; i < CONNECTION_TO_CALL_PROPERTIES.length; i += 2) {
906            if ((CONNECTION_TO_CALL_PROPERTIES[i] & connectionCapabilities) ==
907                    CONNECTION_TO_CALL_PROPERTIES[i]) {
908
909                callProperties |= CONNECTION_TO_CALL_PROPERTIES[i + 1];
910            }
911        }
912        return callProperties;
913    }
914
915    /**
916     * Adds the call to the list of calls tracked by the {@link InCallController}.
917     * @param call The call to add.
918     */
919    private void addCall(Call call) {
920        if (mCallIdMapper.getCallId(call) == null) {
921            mCallIdMapper.addCall(call);
922            call.addListener(mCallListener);
923        }
924    }
925
926    private boolean isBoundToServices() {
927        return !mInCallServices.isEmpty();
928    }
929
930    /**
931     * Removes the specified capability from the set of capabilities bits and returns the new set.
932     */
933    private static int removeCapability(int capabilities, int capability) {
934        return capabilities & ~capability;
935    }
936
937    /**
938     * Dumps the state of the {@link InCallController}.
939     *
940     * @param pw The {@code IndentingPrintWriter} to write the state to.
941     */
942    public void dump(IndentingPrintWriter pw) {
943        pw.println("mInCallServices (InCalls registered):");
944        pw.increaseIndent();
945        for (ComponentName componentName : mInCallServices.keySet()) {
946            pw.println(componentName);
947        }
948        pw.decreaseIndent();
949
950        pw.println("mServiceConnections (InCalls bound):");
951        pw.increaseIndent();
952        for (ComponentName componentName : mServiceConnections.keySet()) {
953            pw.println(componentName);
954        }
955        pw.decreaseIndent();
956    }
957}
958