• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
PstnIncomingCallNotifier(Phone phone)93     PstnIncomingCallNotifier(Phone phone) {
94         Preconditions.checkNotNull(phone);
95 
96         mPhone = phone;
97 
98         registerForNotifications();
99     }
100 
teardown()101     void teardown() {
102         unregisterForNotifications();
103     }
104 
105     /**
106      * Register for notifications from the base phone.
107      */
registerForNotifications()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 
unregisterForNotifications()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      */
handleNewRingingConnection(AsyncResult asyncResult)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 
handleCdmaCallWaiting(AsyncResult asyncResult)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 
handleNewUnknownConnection(AsyncResult asyncResult)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 
addNewUnknownCall(Connection connection)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      */
sendIncomingCallIntent(Connection connection)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      */
findCorrectPhoneAccountHandle()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      */
maybeSwapAnyWithUnknownConnection(Connection unknown)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 
maybeSwapWithUnknownConnection( TelephonyConnection telephonyConnection, Connection unknown)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