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