Home | History | Annotate | Download | only in telephony
      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.services.telephony;
     18 
     19 import android.net.Uri;
     20 import android.os.AsyncResult;
     21 import android.os.Bundle;
     22 import android.os.Handler;
     23 import android.os.Message;
     24 import android.os.SystemClock;
     25 import android.telecom.PhoneAccount;
     26 import android.telecom.PhoneAccountHandle;
     27 import android.telecom.TelecomManager;
     28 import android.text.TextUtils;
     29 
     30 import com.android.internal.telephony.Call;
     31 import com.android.internal.telephony.CallStateException;
     32 import com.android.internal.telephony.Connection;
     33 import com.android.internal.telephony.GsmCdmaPhone;
     34 import com.android.internal.telephony.Phone;
     35 import com.android.internal.telephony.PhoneConstants;
     36 import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
     37 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
     38 import com.android.internal.telephony.imsphone.ImsExternalConnection;
     39 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
     40 import com.android.phone.PhoneUtils;
     41 
     42 import com.google.common.base.Preconditions;
     43 
     44 import java.util.Objects;
     45 
     46 /**
     47  * Listens to incoming-call events from the associated phone object and notifies Telecom upon each
     48  * occurence. One instance of these exists for each of the telephony-based call services.
     49  */
     50 final class PstnIncomingCallNotifier {
     51     /** New ringing connection event code. */
     52     private static final int EVENT_NEW_RINGING_CONNECTION = 100;
     53     private static final int EVENT_CDMA_CALL_WAITING = 101;
     54     private static final int EVENT_UNKNOWN_CONNECTION = 102;
     55 
     56     /** The phone object to listen to. */
     57     private final Phone mPhone;
     58 
     59     /**
     60      * Used to listen to events from {@link #mPhone}.
     61      */
     62     private final Handler mHandler = new Handler() {
     63         @Override
     64         public void handleMessage(Message msg) {
     65             switch(msg.what) {
     66                 case EVENT_NEW_RINGING_CONNECTION:
     67                     handleNewRingingConnection((AsyncResult) msg.obj);
     68                     break;
     69                 case EVENT_CDMA_CALL_WAITING:
     70                     handleCdmaCallWaiting((AsyncResult) msg.obj);
     71                     break;
     72                 case EVENT_UNKNOWN_CONNECTION:
     73                     handleNewUnknownConnection((AsyncResult) msg.obj);
     74                     break;
     75                 default:
     76                     break;
     77             }
     78         }
     79     };
     80 
     81     /**
     82      * Persists the specified parameters and starts listening to phone events.
     83      *
     84      * @param phone The phone object for listening to incoming calls.
     85      */
     86     PstnIncomingCallNotifier(Phone phone) {
     87         Preconditions.checkNotNull(phone);
     88 
     89         mPhone = phone;
     90 
     91         registerForNotifications();
     92     }
     93 
     94     void teardown() {
     95         unregisterForNotifications();
     96     }
     97 
     98     /**
     99      * Register for notifications from the base phone.
    100      */
    101     private void registerForNotifications() {
    102         if (mPhone != null) {
    103             Log.i(this, "Registering: %s", mPhone);
    104             mPhone.registerForNewRingingConnection(mHandler, EVENT_NEW_RINGING_CONNECTION, null);
    105             mPhone.registerForCallWaiting(mHandler, EVENT_CDMA_CALL_WAITING, null);
    106             mPhone.registerForUnknownConnection(mHandler, EVENT_UNKNOWN_CONNECTION, null);
    107         }
    108     }
    109 
    110     private void unregisterForNotifications() {
    111         if (mPhone != null) {
    112             Log.i(this, "Unregistering: %s", mPhone);
    113             mPhone.unregisterForNewRingingConnection(mHandler);
    114             mPhone.unregisterForCallWaiting(mHandler);
    115             mPhone.unregisterForUnknownConnection(mHandler);
    116         }
    117     }
    118 
    119     /**
    120      * Verifies the incoming call and triggers sending the incoming-call intent to Telecom.
    121      *
    122      * @param asyncResult The result object from the new ringing event.
    123      */
    124     private void handleNewRingingConnection(AsyncResult asyncResult) {
    125         Log.d(this, "handleNewRingingConnection");
    126         Connection connection = (Connection) asyncResult.result;
    127         if (connection != null) {
    128             Call call = connection.getCall();
    129 
    130             // Final verification of the ringing state before sending the intent to Telecom.
    131             if (call != null && call.getState().isRinging()) {
    132                 sendIncomingCallIntent(connection);
    133             }
    134         }
    135     }
    136 
    137     private void handleCdmaCallWaiting(AsyncResult asyncResult) {
    138         Log.d(this, "handleCdmaCallWaiting");
    139         CdmaCallWaitingNotification ccwi = (CdmaCallWaitingNotification) asyncResult.result;
    140         Call call = mPhone.getRingingCall();
    141         if (call.getState() == Call.State.WAITING) {
    142             Connection connection = call.getLatestConnection();
    143             if (connection != null) {
    144                 String number = connection.getAddress();
    145                 int presentation = connection.getNumberPresentation();
    146 
    147                 if (presentation != PhoneConstants.PRESENTATION_ALLOWED
    148                         && presentation == ccwi.numberPresentation) {
    149                     // Presentation of number not allowed, but the presentation of the Connection
    150                     // and the call waiting presentation match.
    151                     Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; "
    152                                     + "presentation = %d", presentation);
    153                     sendIncomingCallIntent(connection);
    154                 } else if (!TextUtils.isEmpty(number) && Objects.equals(number, ccwi.number)) {
    155                     // Presentation of the number is allowed, so we ensure the number matches the
    156                     // one in the call waiting information.
    157                     Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; "
    158                             + "number = %s", Log.pii(number));
    159                     sendIncomingCallIntent(connection);
    160                 } else {
    161                     Log.w(this, "handleCdmaCallWaiting: presentation or number do not match, not"
    162                             + " informing telecom of call: %s", ccwi);
    163                 }
    164             }
    165         }
    166     }
    167 
    168     private void handleNewUnknownConnection(AsyncResult asyncResult) {
    169         Log.i(this, "handleNewUnknownConnection");
    170         if (!(asyncResult.result instanceof Connection)) {
    171             Log.w(this, "handleNewUnknownConnection called with non-Connection object");
    172             return;
    173         }
    174         Connection connection = (Connection) asyncResult.result;
    175         if (connection != null) {
    176             // Because there is a handler between telephony and here, it causes this action to be
    177             // asynchronous which means that the call can switch to DISCONNECTED by the time it gets
    178             // to this code. Check here to ensure we are not adding a disconnected or IDLE call.
    179             Call.State state = connection.getState();
    180             if (state == Call.State.DISCONNECTED || state == Call.State.IDLE) {
    181                 Log.i(this, "Skipping new unknown connection because it is idle. " + connection);
    182                 return;
    183             }
    184 
    185             Call call = connection.getCall();
    186             if (call != null && call.getState().isAlive()) {
    187                 addNewUnknownCall(connection);
    188             }
    189         }
    190     }
    191 
    192     private void addNewUnknownCall(Connection connection) {
    193         Log.i(this, "addNewUnknownCall, connection is: %s", connection);
    194 
    195         if (!maybeSwapAnyWithUnknownConnection(connection)) {
    196             Log.i(this, "determined new connection is: %s", connection);
    197             Bundle extras = new Bundle();
    198             if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
    199                     !TextUtils.isEmpty(connection.getAddress())) {
    200                 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null);
    201                 extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, uri);
    202             }
    203             // ImsExternalConnections are keyed by a unique mCallId; include this as an extra on
    204             // the call to addNewUknownCall in Telecom.  This way when the request comes back to the
    205             // TelephonyConnectionService, we will be able to determine which unknown connection is
    206             // being added.
    207             if (connection instanceof ImsExternalConnection) {
    208                 ImsExternalConnection externalConnection = (ImsExternalConnection) connection;
    209                 extras.putInt(ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID,
    210                         externalConnection.getCallId());
    211             }
    212 
    213             // Specifies the time the call was added. This is used by the dialer for analytics.
    214             extras.putLong(TelecomManager.EXTRA_CALL_CREATED_TIME_MILLIS,
    215                     SystemClock.elapsedRealtime());
    216 
    217             PhoneAccountHandle handle = findCorrectPhoneAccountHandle();
    218             if (handle == null) {
    219                 try {
    220                     connection.hangup();
    221                 } catch (CallStateException e) {
    222                     // connection already disconnected. Do nothing
    223                 }
    224             } else {
    225                 TelecomManager.from(mPhone.getContext()).addNewUnknownCall(handle, extras);
    226             }
    227         } else {
    228             Log.i(this, "swapped an old connection, new one is: %s", connection);
    229         }
    230     }
    231 
    232     /**
    233      * Sends the incoming call intent to telecom.
    234      */
    235     private void sendIncomingCallIntent(Connection connection) {
    236         Bundle extras = new Bundle();
    237         if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
    238                 !TextUtils.isEmpty(connection.getAddress())) {
    239             Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null);
    240             extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri);
    241         }
    242 
    243         // Specifies the time the call was added. This is used by the dialer for analytics.
    244         extras.putLong(TelecomManager.EXTRA_CALL_CREATED_TIME_MILLIS,
    245                 SystemClock.elapsedRealtime());
    246 
    247         if (connection.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
    248             if (((ImsPhoneConnection) connection).isRttEnabledForCall()) {
    249                 extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
    250             }
    251         }
    252         PhoneAccountHandle handle = findCorrectPhoneAccountHandle();
    253         if (handle == null) {
    254             try {
    255                 connection.hangup();
    256             } catch (CallStateException e) {
    257                 // connection already disconnected. Do nothing
    258             }
    259         } else {
    260             TelecomManager.from(mPhone.getContext()).addNewIncomingCall(handle, extras);
    261         }
    262     }
    263 
    264     /**
    265      * Returns the PhoneAccount associated with this {@code PstnIncomingCallNotifier}'s phone. On a
    266      * device with No SIM or in airplane mode, it can return an Emergency-only PhoneAccount. If no
    267      * PhoneAccount is registered with telecom, return null.
    268      * @return A valid PhoneAccountHandle that is registered to Telecom or null if there is none
    269      * registered.
    270      */
    271     private PhoneAccountHandle findCorrectPhoneAccountHandle() {
    272         TelecomAccountRegistry telecomAccountRegistry = TelecomAccountRegistry.getInstance(null);
    273         // Check to see if a the SIM PhoneAccountHandle Exists for the Call.
    274         PhoneAccountHandle handle = PhoneUtils.makePstnPhoneAccountHandle(mPhone);
    275         if (telecomAccountRegistry.hasAccountEntryForPhoneAccount(handle)) {
    276             return handle;
    277         }
    278         // The PhoneAccountHandle does not match any PhoneAccount registered in Telecom.
    279         // This is only known to happen if there is no SIM card in the device and the device
    280         // receives an MT call while in ECM. Use the Emergency PhoneAccount to receive the account
    281         // if it exists.
    282         PhoneAccountHandle emergencyHandle =
    283                 PhoneUtils.makePstnPhoneAccountHandleWithPrefix(mPhone, "", true);
    284         if(telecomAccountRegistry.hasAccountEntryForPhoneAccount(emergencyHandle)) {
    285             Log.i(this, "Receiving MT call in ECM. Using Emergency PhoneAccount Instead.");
    286             return emergencyHandle;
    287         }
    288         Log.w(this, "PhoneAccount not found.");
    289         return null;
    290     }
    291 
    292     /**
    293      * Define cait.Connection := com.android.internal.telephony.Connection
    294      *
    295      * Given a previously unknown cait.Connection, check to see if it's likely a replacement for
    296      * another cait.Connnection we already know about. If it is, then we silently swap it out
    297      * underneath within the relevant {@link TelephonyConnection}, using
    298      * {@link TelephonyConnection#setOriginalConnection(Connection)}, and return {@code true}.
    299      * Otherwise, we return {@code false}.
    300      */
    301     private boolean maybeSwapAnyWithUnknownConnection(Connection unknown) {
    302         if (!unknown.isIncoming()) {
    303             TelecomAccountRegistry registry = TelecomAccountRegistry.getInstance(null);
    304             if (registry != null) {
    305                 TelephonyConnectionService service = registry.getTelephonyConnectionService();
    306                 if (service != null) {
    307                     for (android.telecom.Connection telephonyConnection : service
    308                             .getAllConnections()) {
    309                         if (telephonyConnection instanceof TelephonyConnection) {
    310                             if (maybeSwapWithUnknownConnection(
    311                                     (TelephonyConnection) telephonyConnection,
    312                                     unknown)) {
    313                                 return true;
    314                             }
    315                         }
    316                     }
    317                 }
    318             }
    319         }
    320         return false;
    321     }
    322 
    323     private boolean maybeSwapWithUnknownConnection(
    324             TelephonyConnection telephonyConnection,
    325             Connection unknown) {
    326         Connection original = telephonyConnection.getOriginalConnection();
    327         if (original != null && !original.isIncoming()
    328                 && Objects.equals(original.getAddress(), unknown.getAddress())) {
    329             // If the new unknown connection is an external connection, don't swap one with an
    330             // actual connection.  This means a call got pulled away.  We want the actual connection
    331             // to disconnect.
    332             if (unknown instanceof ImsExternalConnection &&
    333                     !(telephonyConnection
    334                             .getOriginalConnection() instanceof ImsExternalConnection)) {
    335                 Log.v(this, "maybeSwapWithUnknownConnection - not swapping regular connection " +
    336                         "with external connection.");
    337                 return false;
    338             }
    339 
    340             telephonyConnection.setOriginalConnection(unknown);
    341 
    342             // Do not call hang up if the original connection is an ImsExternalConnection, it is
    343             // not supported.
    344             if (original instanceof ImsExternalConnection) {
    345                 return true;
    346             }
    347             // If the connection we're replacing was a GSM or CDMA connection, call upon the call
    348             // tracker to perform cleanup of calls.  This ensures that we don't end up with a
    349             // call stuck in the call tracker preventing other calls from being placed.
    350             if (original.getCall() != null && original.getCall().getPhone() != null &&
    351                     original.getCall().getPhone() instanceof GsmCdmaPhone) {
    352 
    353                 GsmCdmaPhone phone = (GsmCdmaPhone) original.getCall().getPhone();
    354                 phone.getCallTracker().cleanupCalls();
    355                 Log.i(this, "maybeSwapWithUnknownConnection - Invoking call tracker cleanup "
    356                         + "for connection: " + original);
    357             }
    358             return true;
    359         }
    360         return false;
    361     }
    362 }
    363