InCallController.java revision 1a40c4f4daf2cdfba3f67383c726f93b85a98b00
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.os.Bundle;
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.CallAudioState;
36import android.telecom.ConnectionService;
37import android.telecom.DefaultDialerManager;
38import android.telecom.InCallService;
39import android.telecom.ParcelableCall;
40import android.telecom.TelecomManager;
41import android.text.TextUtils;
42import android.util.ArrayMap;
43
44import com.android.internal.annotations.VisibleForTesting;
45// TODO: Needed for move to system service: import com.android.internal.R;
46import com.android.internal.telecom.IInCallService;
47import com.android.internal.util.IndentingPrintWriter;
48import com.android.server.telecom.SystemStateProvider.SystemStateListener;
49import com.android.server.telecom.TelecomServiceImpl.DefaultDialerManagerAdapter;
50
51import java.util.ArrayList;
52import java.util.Collection;
53import java.util.LinkedList;
54import java.util.List;
55import java.util.Map;
56import java.util.Objects;
57
58/**
59 * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
60 * can send updates to the in-call app. This class is created and owned by CallsManager and retains
61 * a binding to the {@link IInCallService} (implemented by the in-call app).
62 */
63public final class InCallController extends CallsManagerListenerBase {
64
65    public class InCallServiceConnection {
66        public class Listener {
67            public void onDisconnect(InCallServiceConnection conn) {}
68        }
69
70        protected Listener mListener;
71
72        public boolean connect(Call call) { return false; }
73        public void disconnect() {}
74        public void setHasEmergency(boolean hasEmergency) {}
75        public void setListener(Listener l) {
76            mListener = l;
77        }
78        public void dump(IndentingPrintWriter pw) {}
79    }
80
81    private class InCallServiceBindingConnection extends InCallServiceConnection {
82
83        private final ServiceConnection mServiceConnection = new ServiceConnection() {
84            @Override
85            public void onServiceConnected(ComponentName name, IBinder service) {
86                Log.startSession("ICSBC.oSC");
87                try {
88                    Log.d(this, "onServiceConnected: %s %b %b", name, mIsBound, mIsConnected);
89                    mIsBound = true;
90                    if (mIsConnected) {
91                        // Only proceed if we are supposed to be connected.
92                        onConnected(service);
93                    }
94                } finally {
95                    Log.endSession();
96                }
97            }
98
99            @Override
100            public void onServiceDisconnected(ComponentName name) {
101                Log.startSession("ICSBC.oSD");
102                try {
103                    Log.d(this, "onDisconnected: %s", name);
104                    mIsBound = false;
105                    onDisconnected();
106                } finally {
107                    Log.endSession();
108                }
109            }
110        };
111
112        private final ComponentName mComponentName;
113        private boolean mIsConnected = false;
114        private boolean mIsBound = false;
115
116        public InCallServiceBindingConnection(ComponentName componentName) {
117            mComponentName = componentName;
118        }
119
120        @Override
121        public boolean connect(Call call) {
122            if (mIsConnected) {
123                Log.event(call, Log.Events.INFO, "Already connected, ignoring request.");
124                return true;
125            }
126
127            Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
128            intent.setComponent(mComponentName);
129            if (call != null && !call.isIncoming() && !call.isExternalCall()){
130                intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,
131                        call.getIntentExtras());
132                intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
133                        call.getTargetPhoneAccount());
134            }
135
136            Log.i(this, "Attempting to bind to InCall %s, with %s", mComponentName, intent);
137            if (mContext.bindServiceAsUser(intent, mServiceConnection,
138                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
139                        UserHandle.CURRENT)) {
140                Log.i(this, "It's connecting!");
141                mIsConnected = true;
142            }
143
144            return mIsConnected;
145        }
146
147        @Override
148        public void disconnect() {
149            if (mIsConnected) {
150                mContext.unbindService(mServiceConnection);
151                mIsConnected = false;
152            } else {
153                Log.event(null, Log.Events.INFO, "Already disconnected, ignoring request.");
154            }
155        }
156
157        @Override
158        public void dump(IndentingPrintWriter pw) {
159            pw.append("BindingConnection [");
160            pw.append(mIsConnected ? "" : "not ").append("connected, ");
161            pw.append(mIsBound ? "" : "not ").append("bound]\n");
162        }
163
164        protected void onConnected(IBinder service) {
165            boolean shouldRemainConnected =
166                    InCallController.this.onConnected(mComponentName, service);
167            if (!shouldRemainConnected) {
168                // Sometimes we can opt to disconnect for certain reasons, like if the
169                // InCallService rejected our intialization step, or the calls went away
170                // in the time it took us to bind to the InCallService. In such cases, we go
171                // ahead and disconnect ourselves.
172                disconnect();
173            }
174        }
175
176        protected void onDisconnected() {
177            InCallController.this.onDisconnected(mComponentName);
178            disconnect();  // Unbind explicitly if we get disconnected.
179            if (mListener != null) {
180                mListener.onDisconnect(InCallServiceBindingConnection.this);
181            }
182        }
183    }
184
185    /**
186     * A version of the InCallServiceBindingConnection that proxies all calls to a secondary
187     * connection until it finds an emergency call, or the other connection dies. When one of those
188     * two things happen, this class instance will take over the connection.
189     */
190    private class EmergencyInCallServiceConnection extends InCallServiceBindingConnection {
191        private boolean mIsProxying = true;
192        private boolean mIsConnected = false;
193        private final InCallServiceConnection mSubConnection;
194
195        private Listener mSubListener = new Listener() {
196            @Override
197            public void onDisconnect(InCallServiceConnection subConnection) {
198                if (subConnection == mSubConnection) {
199                    if (mIsConnected && mIsProxying) {
200                        // At this point we know that we need to be connected to the InCallService
201                        // and we are proxying to the sub connection.  However, the sub-connection
202                        // just died so we need to stop proxying and connect to the system in-call
203                        // service instead.
204                        mIsProxying = false;
205                        connect(null);
206                    }
207                }
208            }
209        };
210
211        public EmergencyInCallServiceConnection(
212                ComponentName componentName, InCallServiceConnection subConnection) {
213            super(componentName);
214            mSubConnection = subConnection;
215            if (mSubConnection != null) {
216                mSubConnection.setListener(mSubListener);
217            }
218            mIsProxying = (mSubConnection != null);
219        }
220
221        @Override
222        public boolean connect(Call call) {
223            mIsConnected = true;
224            if (mIsProxying) {
225                if (mSubConnection.connect(call)) {
226                    return true;
227                }
228                // Could not connect to child, stop proxying.
229                mIsProxying = false;
230            }
231
232            // If we are here, we didn't or could not connect to child. So lets connect ourselves.
233            return super.connect(call);
234        }
235
236        @Override
237        public void disconnect() {
238            Log.i(this, "Disconnect forced!");
239            if (mIsProxying) {
240                mSubConnection.disconnect();
241            } else {
242                super.disconnect();
243            }
244            mIsConnected = false;
245        }
246
247        @Override
248        public void setHasEmergency(boolean hasEmergency) {
249            if (hasEmergency) {
250                takeControl();
251            }
252        }
253
254        @Override
255        protected void onDisconnected() {
256            // Save this here because super.onDisconnected() could force us to explicitly
257            // disconnect() as a cleanup step and that sets mIsConnected to false.
258            boolean shouldReconnect = mIsConnected;
259            super.onDisconnected();
260            // We just disconnected.  Check if we are expected to be connected, and reconnect.
261            if (shouldReconnect && !mIsProxying) {
262                connect(null);  // reconnect
263            }
264        }
265
266        @Override
267        public void dump(IndentingPrintWriter pw) {
268            pw.println("Emergency ICS Connection");
269            pw.increaseIndent();
270            pw.print("Emergency: ");
271            super.dump(pw);
272            if (mSubConnection != null) {
273                pw.print("Default-Dialer: ");
274                mSubConnection.dump(pw);
275            }
276            pw.decreaseIndent();
277        }
278
279        /**
280         * Forces the connection to take control from it's subConnection.
281         */
282        private void takeControl() {
283            if (mIsProxying) {
284                mIsProxying = false;
285                if (mIsConnected) {
286                    mSubConnection.disconnect();
287                    super.connect(null);
288                }
289            }
290        }
291    }
292
293    /**
294     * A version of InCallServiceConnection which switches UI between two separate sub-instances of
295     * InCallServicesConnections.
296     */
297    private class CarSwappingInCallServiceConnection extends InCallServiceConnection {
298        private final InCallServiceConnection mDialerConnection;
299        private final InCallServiceConnection mCarModeConnection;
300        private InCallServiceConnection mCurrentConnection;
301        private boolean mIsCarMode = false;
302        private boolean mIsConnected = false;
303
304        public CarSwappingInCallServiceConnection(
305                InCallServiceConnection dialerConnection,
306                InCallServiceConnection carModeConnection) {
307            mDialerConnection = dialerConnection;
308            mCarModeConnection = carModeConnection;
309            mCurrentConnection = getCurrentConnection();
310        }
311
312        public synchronized void setCarMode(boolean isCarMode) {
313            Log.i(this, "carmodechange: " + mIsCarMode + " => " + isCarMode);
314            if (isCarMode != mIsCarMode) {
315                mIsCarMode = isCarMode;
316                InCallServiceConnection newConnection = getCurrentConnection();
317                if (newConnection != mCurrentConnection) {
318                    if (mIsConnected) {
319                        mCurrentConnection.disconnect();
320                        newConnection.connect(null);
321                    }
322                    mCurrentConnection = newConnection;
323                }
324            }
325        }
326
327        @Override
328        public boolean connect(Call call) {
329            if (mIsConnected) {
330                Log.i(this, "already connected");
331                return true;
332            } else {
333                if (mCurrentConnection.connect(call)) {
334                    mIsConnected = true;
335                    return true;
336                }
337            }
338
339            return false;
340        }
341
342        @Override
343        public void disconnect() {
344            if (mIsConnected) {
345                mCurrentConnection.disconnect();
346                mIsConnected = false;
347            } else {
348                Log.i(this, "already disconnected");
349            }
350        }
351
352        @Override
353        public void setHasEmergency(boolean hasEmergency) {
354            if (mDialerConnection != null) {
355                mDialerConnection.setHasEmergency(hasEmergency);
356            }
357            if (mCarModeConnection != null) {
358                mCarModeConnection.setHasEmergency(hasEmergency);
359            }
360        }
361
362        @Override
363        public void dump(IndentingPrintWriter pw) {
364            pw.println("Car Swapping ICS");
365            pw.increaseIndent();
366            if (mDialerConnection != null) {
367                pw.print("Dialer: ");
368                mDialerConnection.dump(pw);
369            }
370            if (mCarModeConnection != null) {
371                pw.print("Car Mode: ");
372                mCarModeConnection.dump(pw);
373            }
374        }
375
376        private InCallServiceConnection getCurrentConnection() {
377            if (mIsCarMode && mCarModeConnection != null) {
378                return mCarModeConnection;
379            } else {
380                return mDialerConnection;
381            }
382        }
383    }
384
385    private class NonUIInCallServiceConnectionCollection extends InCallServiceConnection {
386        private final List<InCallServiceBindingConnection> mSubConnections;
387
388        public NonUIInCallServiceConnectionCollection(
389                List<InCallServiceBindingConnection> subConnections) {
390            mSubConnections = subConnections;
391        }
392
393        @Override
394        public boolean connect(Call call) {
395            for (InCallServiceBindingConnection subConnection : mSubConnections) {
396                subConnection.connect(call);
397            }
398            return true;
399        }
400
401        @Override
402        public void disconnect() {
403            for (InCallServiceBindingConnection subConnection : mSubConnections) {
404                subConnection.disconnect();
405            }
406        }
407
408        @Override
409        public void dump(IndentingPrintWriter pw) {
410            pw.println("Non-UI Connections:");
411            pw.increaseIndent();
412            for (InCallServiceBindingConnection subConnection : mSubConnections) {
413                subConnection.dump(pw);
414            }
415            pw.decreaseIndent();
416        }
417    }
418
419    private final Call.Listener mCallListener = new Call.ListenerBase() {
420        @Override
421        public void onConnectionCapabilitiesChanged(Call call) {
422            updateCall(call);
423        }
424
425        @Override
426        public void onConnectionPropertiesChanged(Call call) {
427            updateCall(call);
428        }
429
430        @Override
431        public void onCannedSmsResponsesLoaded(Call call) {
432            updateCall(call);
433        }
434
435        @Override
436        public void onVideoCallProviderChanged(Call call) {
437            updateCall(call, true /* videoProviderChanged */);
438        }
439
440        @Override
441        public void onStatusHintsChanged(Call call) {
442            updateCall(call);
443        }
444
445        /**
446         * Listens for changes to extras reported by a Telecom {@link Call}.
447         *
448         * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
449         * so we will only trigger an update of the call information if the source of the extras
450         * change was a {@link ConnectionService}.
451         *
452         * @param call The call.
453         * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
454         *               {@link Call#SOURCE_INCALL_SERVICE}).
455         * @param extras The extras.
456         */
457        @Override
458        public void onExtrasChanged(Call call, int source, Bundle extras) {
459            // Do not inform InCallServices of changes which originated there.
460            if (source == Call.SOURCE_INCALL_SERVICE) {
461                return;
462            }
463            updateCall(call);
464        }
465
466        /**
467         * Listens for changes to extras reported by a Telecom {@link Call}.
468         *
469         * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
470         * so we will only trigger an update of the call information if the source of the extras
471         * change was a {@link ConnectionService}.
472         *  @param call The call.
473         * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
474         *               {@link Call#SOURCE_INCALL_SERVICE}).
475         * @param keys The extra key removed
476         */
477        @Override
478        public void onExtrasRemoved(Call call, int source, List<String> keys) {
479            // Do not inform InCallServices of changes which originated there.
480            if (source == Call.SOURCE_INCALL_SERVICE) {
481                return;
482            }
483            updateCall(call);
484        }
485
486        @Override
487        public void onHandleChanged(Call call) {
488            updateCall(call);
489        }
490
491        @Override
492        public void onCallerDisplayNameChanged(Call call) {
493            updateCall(call);
494        }
495
496        @Override
497        public void onVideoStateChanged(Call call) {
498            updateCall(call);
499        }
500
501        @Override
502        public void onTargetPhoneAccountChanged(Call call) {
503            updateCall(call);
504        }
505
506        @Override
507        public void onConferenceableCallsChanged(Call call) {
508            updateCall(call);
509        }
510
511        @Override
512        public void onConnectionEvent(Call call, String event, Bundle extras) {
513            notifyConnectionEvent(call, event, extras);
514        }
515    };
516
517    private final SystemStateListener mSystemStateListener = new SystemStateListener() {
518        @Override
519        public void onCarModeChanged(boolean isCarMode) {
520            if (mInCallServiceConnection != null) {
521                mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
522            }
523        }
524    };
525
526    private static final int IN_CALL_SERVICE_TYPE_INVALID = 0;
527    private static final int IN_CALL_SERVICE_TYPE_DIALER_UI = 1;
528    private static final int IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
529    private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
530    private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
531
532    /** The in-call app implementations, see {@link IInCallService}. */
533    private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
534
535    /**
536     * The {@link ComponentName} of the bound In-Call UI Service.
537     */
538    private ComponentName mInCallUIComponentName;
539
540    private final CallIdMapper mCallIdMapper = new CallIdMapper();
541
542    /** The {@link ComponentName} of the default InCall UI. */
543    private final ComponentName mSystemInCallComponentName;
544
545    private final Context mContext;
546    private final TelecomSystem.SyncRoot mLock;
547    private final CallsManager mCallsManager;
548    private final SystemStateProvider mSystemStateProvider;
549    private final DefaultDialerManagerAdapter mDefaultDialerAdapter;
550    private CarSwappingInCallServiceConnection mInCallServiceConnection;
551    private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections;
552
553    public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
554            SystemStateProvider systemStateProvider,
555            DefaultDialerManagerAdapter defaultDialerAdapter) {
556        mContext = context;
557        mLock = lock;
558        mCallsManager = callsManager;
559        mSystemStateProvider = systemStateProvider;
560        mDefaultDialerAdapter = defaultDialerAdapter;
561
562        Resources resources = mContext.getResources();
563        mSystemInCallComponentName = new ComponentName(
564                resources.getString(R.string.ui_default_package),
565                resources.getString(R.string.incall_default_class));
566
567        mSystemStateProvider.addListener(mSystemStateListener);
568    }
569
570    @Override
571    public void onCallAdded(Call call) {
572        if (!isBoundToServices()) {
573            bindToServices(call);
574        } else {
575            adjustServiceBindingsForEmergency();
576
577            Log.i(this, "onCallAdded: %s", call);
578            // Track the call if we don't already know about it.
579            addCall(call);
580
581            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
582                ComponentName componentName = entry.getKey();
583                IInCallService inCallService = entry.getValue();
584                ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
585                        true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar());
586                try {
587                    inCallService.addCall(parcelableCall);
588                } catch (RemoteException ignored) {
589                }
590            }
591        }
592    }
593
594    @Override
595    public void onCallRemoved(Call call) {
596        Log.i(this, "onCallRemoved: %s", call);
597        if (mCallsManager.getCalls().isEmpty()) {
598            /** Let's add a 2 second delay before we send unbind to the services to hopefully
599             *  give them enough time to process all the pending messages.
600             */
601            Handler handler = new Handler(Looper.getMainLooper());
602            handler.postDelayed(new Runnable("ICC.oCR") {
603                @Override
604                public void loggedRun() {
605                    synchronized (mLock) {
606                        // Check again to make sure there are no active calls.
607                        if (mCallsManager.getCalls().isEmpty()) {
608                            unbindFromServices();
609                        }
610                    }
611                }
612            }.prepare(), Timeouts.getCallRemoveUnbindInCallServicesDelay(
613                            mContext.getContentResolver()));
614        }
615        call.removeListener(mCallListener);
616        mCallIdMapper.removeCall(call);
617    }
618
619    @Override
620    public void onExternalCallChanged(Call call, boolean isExternalCall) {
621        Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
622        // TODO: Need to add logic which ensures changes to a call's external state adds or removes
623        // the call from the InCallServices depending on whether they support external calls.
624    }
625
626    @Override
627    public void onCallStateChanged(Call call, int oldState, int newState) {
628        updateCall(call);
629    }
630
631    @Override
632    public void onConnectionServiceChanged(
633            Call call,
634            ConnectionServiceWrapper oldService,
635            ConnectionServiceWrapper newService) {
636        updateCall(call);
637    }
638
639    @Override
640    public void onCallAudioStateChanged(CallAudioState oldCallAudioState,
641            CallAudioState newCallAudioState) {
642        if (!mInCallServices.isEmpty()) {
643            Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
644                    newCallAudioState);
645            for (IInCallService inCallService : mInCallServices.values()) {
646                try {
647                    inCallService.onCallAudioStateChanged(newCallAudioState);
648                } catch (RemoteException ignored) {
649                }
650            }
651        }
652    }
653
654    @Override
655    public void onCanAddCallChanged(boolean canAddCall) {
656        if (!mInCallServices.isEmpty()) {
657            Log.i(this, "onCanAddCallChanged : %b", canAddCall);
658            for (IInCallService inCallService : mInCallServices.values()) {
659                try {
660                    inCallService.onCanAddCallChanged(canAddCall);
661                } catch (RemoteException ignored) {
662                }
663            }
664        }
665    }
666
667    void onPostDialWait(Call call, String remaining) {
668        if (!mInCallServices.isEmpty()) {
669            Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
670            for (IInCallService inCallService : mInCallServices.values()) {
671                try {
672                    inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
673                } catch (RemoteException ignored) {
674                }
675            }
676        }
677    }
678
679    @Override
680    public void onIsConferencedChanged(Call call) {
681        Log.d(this, "onIsConferencedChanged %s", call);
682        updateCall(call);
683    }
684
685    void bringToForeground(boolean showDialpad) {
686        if (!mInCallServices.isEmpty()) {
687            for (IInCallService inCallService : mInCallServices.values()) {
688                try {
689                    inCallService.bringToForeground(showDialpad);
690                } catch (RemoteException ignored) {
691                }
692            }
693        } else {
694            Log.w(this, "Asking to bring unbound in-call UI to foreground.");
695        }
696    }
697
698    void silenceRinger() {
699        if (!mInCallServices.isEmpty()) {
700            for (IInCallService inCallService : mInCallServices.values()) {
701                try {
702                    inCallService.silenceRinger();
703                } catch (RemoteException ignored) {
704                }
705            }
706        }
707    }
708
709    private void notifyConnectionEvent(Call call, String event, Bundle extras) {
710        if (!mInCallServices.isEmpty()) {
711            for (IInCallService inCallService : mInCallServices.values()) {
712                try {
713                    inCallService.onConnectionEvent(mCallIdMapper.getCallId(call), event, extras);
714                } catch (RemoteException ignored) {
715                }
716            }
717        }
718    }
719
720    /**
721     * Unbinds an existing bound connection to the in-call app.
722     */
723    private void unbindFromServices() {
724        if (isBoundToServices()) {
725            mInCallServiceConnection.disconnect();
726            mInCallServiceConnection = null;
727            mNonUIInCallServiceConnections.disconnect();
728            mNonUIInCallServiceConnections = null;
729        }
730    }
731
732    /**
733     * Binds to all the UI-providing InCallService as well as system-implemented non-UI
734     * InCallServices. Method-invoker must check {@link #isBoundToServices()} before invoking.
735     *
736     * @param call The newly added call that triggered the binding to the in-call services.
737     */
738    @VisibleForTesting
739    public void bindToServices(Call call) {
740        InCallServiceConnection dialerInCall = null;
741        ComponentName defaultDialerComponent = getDefaultDialerComponent();
742        Log.i(this, "defaultDialer: " + defaultDialerComponent);
743        if (defaultDialerComponent != null &&
744                !defaultDialerComponent.equals(mSystemInCallComponentName)) {
745            dialerInCall = new InCallServiceBindingConnection(defaultDialerComponent);
746        }
747        Log.i(this, "defaultDialer: " + dialerInCall);
748
749        EmergencyInCallServiceConnection systemInCall =
750                new EmergencyInCallServiceConnection(mSystemInCallComponentName, dialerInCall);
751        systemInCall.setHasEmergency(mCallsManager.hasEmergencyCall());
752
753        InCallServiceConnection carModeInCall = null;
754        ComponentName carModeComponent = getCarModeComponent();
755        if (carModeComponent != null &&
756                !carModeComponent.equals(mSystemInCallComponentName)) {
757            carModeInCall = new InCallServiceBindingConnection(carModeComponent);
758        }
759
760        mInCallServiceConnection =
761            new CarSwappingInCallServiceConnection(systemInCall, carModeInCall);
762        mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
763        mInCallServiceConnection.connect(call);
764
765
766        List<ComponentName> nonUIInCallComponents =
767                getInCallServiceComponents(null, IN_CALL_SERVICE_TYPE_NON_UI);
768        List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
769        for (ComponentName componentName : nonUIInCallComponents) {
770            nonUIInCalls.add(new InCallServiceBindingConnection(componentName));
771        }
772        mNonUIInCallServiceConnections = new NonUIInCallServiceConnectionCollection(nonUIInCalls);
773        mNonUIInCallServiceConnections.connect(call);
774    }
775
776    private ComponentName getDefaultDialerComponent() {
777        String packageName = mDefaultDialerAdapter.getDefaultDialerApplication(
778                mContext, mCallsManager.getCurrentUserHandle().getIdentifier());
779        Log.d(this, "Default Dialer package: " + packageName);
780
781        return getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_DIALER_UI);
782    }
783
784    private ComponentName getCarModeComponent() {
785        return getInCallServiceComponent(null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
786    }
787
788    private ComponentName getInCallServiceComponent(String packageName, int type) {
789        List<ComponentName> list = getInCallServiceComponents(packageName, type);
790        if (list != null && !list.isEmpty()) {
791            return list.get(0);
792        }
793        return null;
794    }
795
796    private List<ComponentName> getInCallServiceComponents(String packageName, int type) {
797        List<ComponentName> retval = new LinkedList<>();
798
799        Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
800        if (packageName != null) {
801            serviceIntent.setPackage(packageName);
802        }
803
804        PackageManager packageManager = mContext.getPackageManager();
805        for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
806                serviceIntent,
807                PackageManager.GET_META_DATA,
808                mCallsManager.getCurrentUserHandle().getIdentifier())) {
809            ServiceInfo serviceInfo = entry.serviceInfo;
810
811            if (serviceInfo != null) {
812                if (type == 0 || type == getInCallServiceType(entry.serviceInfo, packageManager)) {
813                    retval.add(new ComponentName(serviceInfo.packageName, serviceInfo.name));
814                }
815            }
816        }
817
818        return retval;
819    }
820
821    private boolean shouldUseCarModeUI() {
822        return mSystemStateProvider.isCarMode();
823    }
824
825    /**
826     * Returns the type of InCallService described by the specified serviceInfo.
827     */
828    private int getInCallServiceType(ServiceInfo serviceInfo, PackageManager packageManager) {
829        // Verify that the InCallService requires the BIND_INCALL_SERVICE permission which
830        // enforces that only Telecom can bind to it.
831        boolean hasServiceBindPermission = serviceInfo.permission != null &&
832                serviceInfo.permission.equals(
833                        Manifest.permission.BIND_INCALL_SERVICE);
834        if (!hasServiceBindPermission) {
835            Log.w(this, "InCallService does not require BIND_INCALL_SERVICE permission: " +
836                    serviceInfo.packageName);
837            return IN_CALL_SERVICE_TYPE_INVALID;
838        }
839
840        if (mSystemInCallComponentName.getPackageName().equals(serviceInfo.packageName) &&
841                mSystemInCallComponentName.getClassName().equals(serviceInfo.name)) {
842            return IN_CALL_SERVICE_TYPE_SYSTEM_UI;
843        }
844
845        // Check to see if the service is a car-mode UI type by checking that it has the
846        // CONTROL_INCALL_EXPERIENCE (to verify it is a system app) and that it has the
847        // car-mode UI metadata.
848        boolean hasControlInCallPermission = packageManager.checkPermission(
849                Manifest.permission.CONTROL_INCALL_EXPERIENCE,
850                serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
851        boolean isCarModeUIService = serviceInfo.metaData != null &&
852                serviceInfo.metaData.getBoolean(
853                        TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false) &&
854                hasControlInCallPermission;
855        if (isCarModeUIService) {
856            return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
857        }
858
859
860        // Check to see that it is the default dialer package
861        boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
862                mDefaultDialerAdapter.getDefaultDialerApplication(
863                    mContext, mCallsManager.getCurrentUserHandle().getIdentifier()));
864        boolean isUIService = serviceInfo.metaData != null &&
865                serviceInfo.metaData.getBoolean(
866                        TelecomManager.METADATA_IN_CALL_SERVICE_UI, false);
867        if (isDefaultDialerPackage && isUIService) {
868            return IN_CALL_SERVICE_TYPE_DIALER_UI;
869        }
870
871        // Also allow any in-call service that has the control-experience permission (to ensure
872        // that it is a system app) and doesn't claim to show any UI.
873        if (hasControlInCallPermission && !isUIService) {
874            return IN_CALL_SERVICE_TYPE_NON_UI;
875        }
876
877        // Anything else that remains, we will not bind to.
878        Log.i(this, "Skipping binding to %s:%s, control: %b, car-mode: %b, ui: %b",
879                serviceInfo.packageName, serviceInfo.name, hasControlInCallPermission,
880                isCarModeUIService, isUIService);
881        return IN_CALL_SERVICE_TYPE_INVALID;
882    }
883
884    private void adjustServiceBindingsForEmergency() {
885        // The connected UI is not the system UI, so lets check if we should switch them
886        // if there exists an emergency number.
887        if (mCallsManager.hasEmergencyCall()) {
888            mInCallServiceConnection.setHasEmergency(true);
889        }
890    }
891
892    /**
893     * Persists the {@link IInCallService} instance and starts the communication between
894     * this class and in-call app by sending the first update to in-call app. This method is
895     * called after a successful binding connection is established.
896     *
897     * @param componentName The service {@link ComponentName}.
898     * @param service The {@link IInCallService} implementation.
899     * @return True if we successfully connected.
900     */
901    private boolean onConnected(ComponentName componentName, IBinder service) {
902        Trace.beginSection("onConnected: " + componentName);
903        Log.i(this, "onConnected to %s", componentName);
904
905        IInCallService inCallService = IInCallService.Stub.asInterface(service);
906        mInCallServices.put(componentName, inCallService);
907
908        try {
909            inCallService.setInCallAdapter(
910                    new InCallAdapter(
911                            mCallsManager,
912                            mCallIdMapper,
913                            mLock,
914                            componentName.getPackageName()));
915        } catch (RemoteException e) {
916            Log.e(this, e, "Failed to set the in-call adapter.");
917            Trace.endSection();
918            return false;
919        }
920
921        // Upon successful connection, send the state of the world to the service.
922        List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls());
923        if (!calls.isEmpty()) {
924            Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
925                    componentName);
926            for (Call call : calls) {
927                try {
928                    // Track the call if we don't already know about it.
929                    addCall(call);
930                    inCallService.addCall(ParcelableCallUtils.toParcelableCall(
931                            call,
932                            true /* includeVideoProvider */,
933                            mCallsManager.getPhoneAccountRegistrar()));
934                } catch (RemoteException ignored) {
935                }
936            }
937            try {
938                inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
939                inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
940            } catch (RemoteException ignored) {
941            }
942        } else {
943            return false;
944        }
945        Trace.endSection();
946        return true;
947    }
948
949    /**
950     * Cleans up an instance of in-call app after the service has been unbound.
951     *
952     * @param disconnectedComponent The {@link ComponentName} of the service which disconnected.
953     */
954    private void onDisconnected(ComponentName disconnectedComponent) {
955        Log.i(this, "onDisconnected from %s", disconnectedComponent);
956
957        mInCallServices.remove(disconnectedComponent);
958    }
959
960    /**
961     * Informs all {@link InCallService} instances of the updated call information.
962     *
963     * @param call The {@link Call}.
964     */
965    private void updateCall(Call call) {
966        updateCall(call, false /* videoProviderChanged */);
967    }
968
969    /**
970     * Informs all {@link InCallService} instances of the updated call information.
971     *
972     * @param call The {@link Call}.
973     * @param videoProviderChanged {@code true} if the video provider changed, {@code false}
974     *      otherwise.
975     */
976    private void updateCall(Call call, boolean videoProviderChanged) {
977        if (!mInCallServices.isEmpty()) {
978            ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
979                    call,
980                    videoProviderChanged /* includeVideoProvider */,
981                    mCallsManager.getPhoneAccountRegistrar());
982            Log.i(this, "Sending updateCall %s ==> %s", call, parcelableCall);
983            List<ComponentName> componentsUpdated = new ArrayList<>();
984            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
985                ComponentName componentName = entry.getKey();
986                IInCallService inCallService = entry.getValue();
987                componentsUpdated.add(componentName);
988                try {
989                    inCallService.updateCall(parcelableCall);
990                } catch (RemoteException ignored) {
991                }
992            }
993            Log.i(this, "Components updated: %s", componentsUpdated);
994        }
995    }
996
997    /**
998     * Adds the call to the list of calls tracked by the {@link InCallController}.
999     * @param call The call to add.
1000     */
1001    private void addCall(Call call) {
1002        if (mCallIdMapper.getCallId(call) == null) {
1003            mCallIdMapper.addCall(call);
1004            call.addListener(mCallListener);
1005        }
1006    }
1007
1008    private boolean isBoundToServices() {
1009        return mInCallServiceConnection != null;
1010    }
1011
1012    /**
1013     * Dumps the state of the {@link InCallController}.
1014     *
1015     * @param pw The {@code IndentingPrintWriter} to write the state to.
1016     */
1017    public void dump(IndentingPrintWriter pw) {
1018        pw.println("mInCallServices (InCalls registered):");
1019        pw.increaseIndent();
1020        for (ComponentName componentName : mInCallServices.keySet()) {
1021            pw.println(componentName);
1022        }
1023        pw.decreaseIndent();
1024
1025        pw.println("ServiceConnections (InCalls bound):");
1026        pw.increaseIndent();
1027        if (mInCallServiceConnection != null) {
1028            mInCallServiceConnection.dump(pw);
1029        }
1030        pw.decreaseIndent();
1031    }
1032
1033    public boolean doesConnectedDialerSupportRinging() {
1034        String ringingPackage =  null;
1035        if (mInCallUIComponentName != null) {
1036            ringingPackage = mInCallUIComponentName.getPackageName().trim();
1037        }
1038
1039        if (TextUtils.isEmpty(ringingPackage)) {
1040            // The current in-call UI returned nothing, so lets use the default dialer.
1041            ringingPackage = DefaultDialerManager.getDefaultDialerApplication(
1042                    mContext, UserHandle.USER_CURRENT);
1043        }
1044        if (TextUtils.isEmpty(ringingPackage)) {
1045            return false;
1046        }
1047
1048        Intent intent = new Intent(InCallService.SERVICE_INTERFACE)
1049            .setPackage(ringingPackage);
1050        List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServicesAsUser(
1051                intent, PackageManager.GET_META_DATA,
1052                mCallsManager.getCurrentUserHandle().getIdentifier());
1053        if (entries.isEmpty()) {
1054            return false;
1055        }
1056
1057        ResolveInfo info = entries.get(0);
1058        if (info.serviceInfo == null || info.serviceInfo.metaData == null) {
1059            return false;
1060        }
1061
1062        return info.serviceInfo.metaData
1063                .getBoolean(TelecomManager.METADATA_IN_CALL_SERVICE_RINGING, false);
1064    }
1065
1066    private List<Call> orderCallsWithChildrenFirst(Collection<Call> calls) {
1067        LinkedList<Call> parentCalls = new LinkedList<>();
1068        LinkedList<Call> childCalls = new LinkedList<>();
1069        for (Call call : calls) {
1070            if (call.getChildCalls().size() > 0) {
1071                parentCalls.add(call);
1072            } else {
1073                childCalls.add(call);
1074            }
1075        }
1076        childCalls.addAll(parentCalls);
1077        return childCalls;
1078    }
1079}
1080