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