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.NumberVerificationManager;
41 import com.android.phone.PhoneUtils;
42 import com.android.telephony.Rlog;
43 
44 import java.util.List;
45 import java.util.Objects;
46 import java.util.stream.Collectors;
47 
48 /**
49  * Listens to incoming-call events from the associated phone object and notifies Telecom upon each
50  * occurence. One instance of these exists for each of the telephony-based call services.
51  */
52 final class PstnIncomingCallNotifier {
53     private static final String LOG_TAG = "PstnIncomingCallNotifier";
54 
55     /** New ringing connection event code. */
56     private static final int EVENT_NEW_RINGING_CONNECTION = 100;
57     private static final int EVENT_CDMA_CALL_WAITING = 101;
58     private static final int EVENT_UNKNOWN_CONNECTION = 102;
59 
60     /**
61      * The max amount of time to wait before hanging up a call that was for number verification.
62      *
63      * The delay is so that the remote end has time to hang up the call after receiving the
64      * verification signal so that the call doesn't go to voicemail.
65      */
66     private static final int MAX_NUMBER_VERIFICATION_HANGUP_DELAY_MILLIS = 10000;
67 
68     /** The phone object to listen to. */
69     private final Phone mPhone;
70 
71     /**
72      * Used to listen to events from {@link #mPhone}.
73      */
74     private final Handler mHandler = new Handler() {
75         @Override
76         public void handleMessage(Message msg) {
77             switch(msg.what) {
78                 case EVENT_NEW_RINGING_CONNECTION:
79                     handleNewRingingConnection((AsyncResult) msg.obj);
80                     break;
81                 case EVENT_CDMA_CALL_WAITING:
82                     handleCdmaCallWaiting((AsyncResult) msg.obj);
83                     break;
84                 case EVENT_UNKNOWN_CONNECTION:
85                     handleNewUnknownConnection((AsyncResult) msg.obj);
86                     break;
87                 default:
88                     break;
89             }
90         }
91     };
92 
93     /**
94      * Persists the specified parameters and starts listening to phone events.
95      *
96      * @param phone The phone object for listening to incoming calls.
97      */
PstnIncomingCallNotifier(Phone phone)98     PstnIncomingCallNotifier(Phone phone) {
99         if (phone == null) {
100             throw new NullPointerException();
101         }
102 
103         mPhone = phone;
104 
105         registerForNotifications();
106     }
107 
teardown()108     void teardown() {
109         unregisterForNotifications();
110     }
111 
112     /**
113      * Register for notifications from the base phone.
114      */
registerForNotifications()115     private void registerForNotifications() {
116         if (mPhone != null) {
117             Log.i(this, "Registering: %s", mPhone);
118             mPhone.registerForNewRingingConnection(mHandler, EVENT_NEW_RINGING_CONNECTION, null);
119             mPhone.registerForCallWaiting(mHandler, EVENT_CDMA_CALL_WAITING, null);
120             mPhone.registerForUnknownConnection(mHandler, EVENT_UNKNOWN_CONNECTION, null);
121         }
122     }
123 
unregisterForNotifications()124     private void unregisterForNotifications() {
125         if (mPhone != null) {
126             Log.i(this, "Unregistering: %s", mPhone);
127             mPhone.unregisterForNewRingingConnection(mHandler);
128             mPhone.unregisterForCallWaiting(mHandler);
129             mPhone.unregisterForUnknownConnection(mHandler);
130         }
131     }
132 
133     /**
134      * Verifies the incoming call and triggers sending the incoming-call intent to Telecom.
135      *
136      * @param asyncResult The result object from the new ringing event.
137      */
handleNewRingingConnection(AsyncResult asyncResult)138     private void handleNewRingingConnection(AsyncResult asyncResult) {
139         Log.d(this, "handleNewRingingConnection");
140         Connection connection = (Connection) asyncResult.result;
141         if (connection != null) {
142             Call call = connection.getCall();
143             // Check if we have a pending number verification request.
144             if (connection.getAddress() != null) {
145                 if (NumberVerificationManager.getInstance()
146                         .checkIncomingCall(connection.getAddress())) {
147                     // Disconnect the call if it matches, after a delay
148                     mHandler.postDelayed(() -> {
149                         try {
150                             connection.hangup();
151                         } catch (CallStateException e) {
152                             Log.i(this, "Remote end hung up call verification call");
153                         }
154                         // TODO: use an app-supplied delay (needs new API), not to exceed the
155                         // existing max.
156                     }, MAX_NUMBER_VERIFICATION_HANGUP_DELAY_MILLIS);
157                     return;
158                 }
159             }
160 
161             // Final verification of the ringing state before sending the intent to Telecom.
162             if (call != null && call.getState().isRinging()) {
163                 sendIncomingCallIntent(connection);
164             }
165         }
166     }
167 
handleCdmaCallWaiting(AsyncResult asyncResult)168     private void handleCdmaCallWaiting(AsyncResult asyncResult) {
169         Log.d(this, "handleCdmaCallWaiting");
170         CdmaCallWaitingNotification ccwi = (CdmaCallWaitingNotification) asyncResult.result;
171         Call call = mPhone.getRingingCall();
172         if (call.getState() == Call.State.WAITING) {
173             Connection connection = call.getLatestConnection();
174             if (connection != null) {
175                 String number = connection.getAddress();
176                 int presentation = connection.getNumberPresentation();
177 
178                 if (presentation != PhoneConstants.PRESENTATION_ALLOWED
179                         && presentation == ccwi.numberPresentation) {
180                     // Presentation of number not allowed, but the presentation of the Connection
181                     // and the call waiting presentation match.
182                     Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; "
183                                     + "presentation = %d", presentation);
184                     sendIncomingCallIntent(connection);
185                 } else if (!TextUtils.isEmpty(number) && Objects.equals(number, ccwi.number)) {
186                     // Presentation of the number is allowed, so we ensure the number matches the
187                     // one in the call waiting information.
188                     Log.i(this, "handleCdmaCallWaiting: inform telecom of waiting call; "
189                             + "number = %s", Rlog.pii(LOG_TAG, number));
190                     sendIncomingCallIntent(connection);
191                 } else {
192                     Log.w(this, "handleCdmaCallWaiting: presentation or number do not match, not"
193                             + " informing telecom of call: %s", ccwi);
194                 }
195             }
196         }
197     }
198 
handleNewUnknownConnection(AsyncResult asyncResult)199     private void handleNewUnknownConnection(AsyncResult asyncResult) {
200         Log.i(this, "handleNewUnknownConnection");
201         if (!(asyncResult.result instanceof Connection)) {
202             Log.w(this, "handleNewUnknownConnection called with non-Connection object");
203             return;
204         }
205         Connection connection = (Connection) asyncResult.result;
206         if (connection != null) {
207             // Because there is a handler between telephony and here, it causes this action to be
208             // asynchronous which means that the call can switch to DISCONNECTED by the time it gets
209             // to this code. Check here to ensure we are not adding a disconnected or IDLE call.
210             Call.State state = connection.getState();
211             if (state == Call.State.DISCONNECTED || state == Call.State.IDLE) {
212                 Log.i(this, "Skipping new unknown connection because it is idle. " + connection);
213                 return;
214             }
215 
216             Call call = connection.getCall();
217             if (call != null && call.getState().isAlive()) {
218                 addNewUnknownCall(connection);
219             }
220         }
221     }
222 
addNewUnknownCall(Connection connection)223     private void addNewUnknownCall(Connection connection) {
224         Log.i(this, "addNewUnknownCall, connection is: %s", connection);
225 
226         if (!maybeSwapAnyWithUnknownConnection(connection)) {
227             Log.i(this, "determined new connection is: %s", connection);
228             Bundle extras = new Bundle();
229             if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
230                     !TextUtils.isEmpty(connection.getAddress())) {
231                 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null);
232                 extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, uri);
233             }
234             // ImsExternalConnections are keyed by a unique mCallId; include this as an extra on
235             // the call to addNewUknownCall in Telecom.  This way when the request comes back to the
236             // TelephonyConnectionService, we will be able to determine which unknown connection is
237             // being added.
238             if (connection instanceof ImsExternalConnection) {
239                 ImsExternalConnection externalConnection = (ImsExternalConnection) connection;
240                 extras.putInt(ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID,
241                         externalConnection.getCallId());
242             }
243 
244             // Specifies the time the call was added. This is used by the dialer for analytics.
245             extras.putLong(TelecomManager.EXTRA_CALL_CREATED_TIME_MILLIS,
246                     SystemClock.elapsedRealtime());
247 
248             PhoneAccountHandle handle = findCorrectPhoneAccountHandle();
249             if (handle == null) {
250                 try {
251                     connection.hangup();
252                 } catch (CallStateException e) {
253                     // connection already disconnected. Do nothing
254                 }
255             } else {
256                 TelecomManager tm = mPhone.getContext().getSystemService(TelecomManager.class);
257                 tm.addNewUnknownCall(handle, extras);
258             }
259         } else {
260             Log.i(this, "swapped an old connection, new one is: %s", connection);
261         }
262     }
263 
264     /**
265      * Sends the incoming call intent to telecom.
266      */
sendIncomingCallIntent(Connection connection)267     private void sendIncomingCallIntent(Connection connection) {
268         Bundle extras = new Bundle();
269         if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
270                 !TextUtils.isEmpty(connection.getAddress())) {
271             Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null);
272             extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri);
273         }
274 
275         // Specifies the time the call was added. This is used by the dialer for analytics.
276         extras.putLong(TelecomManager.EXTRA_CALL_CREATED_TIME_MILLIS,
277                 SystemClock.elapsedRealtime());
278 
279         if (connection.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
280             if (((ImsPhoneConnection) connection).isRttEnabledForCall()) {
281                 extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
282             }
283             if (((ImsPhoneConnection) connection).isIncomingCallAutoRejected()) {
284                 extras.putString(TelecomManager.EXTRA_CALL_DISCONNECT_MESSAGE,
285                         "Call Dropped by lower layers");
286             }
287         }
288 
289         PhoneAccountHandle handle = findCorrectPhoneAccountHandle();
290         if (handle == null) {
291             try {
292                 connection.hangup();
293             } catch (CallStateException e) {
294                 // connection already disconnected. Do nothing
295             }
296             Log.wtf(LOG_TAG, "sendIncomingCallIntent: failed to add new call because no phone"
297                     + " account could be found for the call");
298         } else {
299             TelecomManager tm = mPhone.getContext().getSystemService(TelecomManager.class);
300             try {
301                 if (connection.isMultiparty()) {
302                     tm.addNewIncomingConference(handle, extras);
303                 } else {
304                     tm.addNewIncomingCall(handle, extras);
305                 }
306             } catch (SecurityException se) {
307                 // If we get a security exception, the most likely cause is:
308                 // "This PhoneAccountHandle is not registered for this user"
309                 // If this happens, then it means that for whatever reason the phone account which
310                 // we are trying to use to add the new incoming call no longer exists in Telecom.
311                 // This can happen if the handle of the phone account changes.  The likely root
312                 // cause of this would be a change in active SIM profile for an MVNO style carrier
313                 // which aggregates multiple carriers together.
314 
315                 // We will log a list of the available handles ourselves here; PhoneAccountHandle
316                 // oscures the ID in the toString.  Rlog.pii will do a secure hash on userdebug
317                 // builds so at least we could tell if the handle we tried is different from the one
318                 // which we attempted to use.
319                 List<PhoneAccountHandle> handles = tm.getCallCapablePhoneAccounts();
320                 String availableHandles = handles.stream()
321                         .map(h -> "[" + h.getComponentName() + " "
322                                 + Rlog.pii(LOG_TAG, h.getId()) + "]")
323                         .collect(Collectors.joining(","));
324                 String attemptedHandle = "[" + handle.getComponentName() + " "
325                         + Rlog.pii(LOG_TAG, handle.getId()) + "]";
326                 Log.wtf(LOG_TAG, "sendIncomingCallIntent: failed to add new call " + connection
327                         + " because the handle " + attemptedHandle
328                         + " is not in the list of registered handles " + availableHandles
329                         + " - call was rejected.");
330 
331                 // Since the phone account handle we're trying to use is not valid, we have no other
332                 // recourse but to reject the incoming call.
333                 try {
334                     connection.hangup();
335                 } catch (CallStateException e) {
336                     // connection already disconnected. Do nothing
337                 }
338             }
339         }
340     }
341 
342     /**
343      * Returns the PhoneAccount associated with this {@code PstnIncomingCallNotifier}'s phone. On a
344      * device with No SIM or in airplane mode, it can return an Emergency-only PhoneAccount. If no
345      * PhoneAccount is registered with telecom, return null.
346      * @return A valid PhoneAccountHandle that is registered to Telecom or null if there is none
347      * registered.
348      */
findCorrectPhoneAccountHandle()349     private PhoneAccountHandle findCorrectPhoneAccountHandle() {
350         TelecomAccountRegistry telecomAccountRegistry = TelecomAccountRegistry.getInstance(null);
351         // Check to see if a the SIM PhoneAccountHandle Exists for the Call.
352         PhoneAccountHandle handle = PhoneUtils.makePstnPhoneAccountHandle(mPhone);
353         if (telecomAccountRegistry.hasAccountEntryForPhoneAccount(handle)) {
354             return handle;
355         }
356         // The PhoneAccountHandle does not match any PhoneAccount registered in Telecom.
357         // This is only known to happen if there is no SIM card in the device and the device
358         // receives an MT call while in ECM. Use the Emergency PhoneAccount to receive the account
359         // if it exists.
360         PhoneAccountHandle emergencyHandle =
361                 PhoneUtils.makePstnPhoneAccountHandleWithPrefix(mPhone, "", true);
362         if(telecomAccountRegistry.hasAccountEntryForPhoneAccount(emergencyHandle)) {
363             Log.i(this, "Receiving MT call in ECM. Using Emergency PhoneAccount Instead.");
364             return emergencyHandle;
365         }
366         Log.w(this, "PhoneAccount not found.");
367         return null;
368     }
369 
370     /**
371      * Define cait.Connection := com.android.internal.telephony.Connection
372      *
373      * Given a previously unknown cait.Connection, check to see if it's likely a replacement for
374      * another cait.Connnection we already know about. If it is, then we silently swap it out
375      * underneath within the relevant {@link TelephonyConnection}, using
376      * {@link TelephonyConnection#setOriginalConnection(Connection)}, and return {@code true}.
377      * Otherwise, we return {@code false}.
378      */
maybeSwapAnyWithUnknownConnection(Connection unknown)379     private boolean maybeSwapAnyWithUnknownConnection(Connection unknown) {
380         if (!unknown.isIncoming()) {
381             TelecomAccountRegistry registry = TelecomAccountRegistry.getInstance(null);
382             if (registry != null) {
383                 TelephonyConnectionService service = registry.getTelephonyConnectionService();
384                 if (service != null) {
385                     for (android.telecom.Connection telephonyConnection : service
386                             .getAllConnections()) {
387                         if (telephonyConnection instanceof TelephonyConnection) {
388                             if (maybeSwapWithUnknownConnection(
389                                     (TelephonyConnection) telephonyConnection,
390                                     unknown)) {
391                                 return true;
392                             }
393                         }
394                     }
395                 }
396             }
397         }
398         return false;
399     }
400 
maybeSwapWithUnknownConnection( TelephonyConnection telephonyConnection, Connection unknown)401     private boolean maybeSwapWithUnknownConnection(
402             TelephonyConnection telephonyConnection,
403             Connection unknown) {
404         Connection original = telephonyConnection.getOriginalConnection();
405         if (original != null && !original.isIncoming()
406                 && Objects.equals(original.getAddress(), unknown.getAddress())) {
407             // If the new unknown connection is an external connection, don't swap one with an
408             // actual connection.  This means a call got pulled away.  We want the actual connection
409             // to disconnect.
410             if (unknown instanceof ImsExternalConnection &&
411                     !(telephonyConnection
412                             .getOriginalConnection() instanceof ImsExternalConnection)) {
413                 Log.v(this, "maybeSwapWithUnknownConnection - not swapping regular connection " +
414                         "with external connection.");
415                 return false;
416             }
417 
418             telephonyConnection.setOriginalConnection(unknown);
419 
420             // Do not call hang up if the original connection is an ImsExternalConnection, it is
421             // not supported.
422             if (original instanceof ImsExternalConnection) {
423                 return true;
424             }
425             // If the connection we're replacing was a GSM or CDMA connection, call upon the call
426             // tracker to perform cleanup of calls.  This ensures that we don't end up with a
427             // call stuck in the call tracker preventing other calls from being placed.
428             if (original.getCall() != null && original.getCall().getPhone() != null &&
429                     original.getCall().getPhone() instanceof GsmCdmaPhone) {
430 
431                 GsmCdmaPhone phone = (GsmCdmaPhone) original.getCall().getPhone();
432                 phone.getCallTracker().cleanupCalls();
433                 Log.i(this, "maybeSwapWithUnknownConnection - Invoking call tracker cleanup "
434                         + "for connection: " + original);
435             }
436             return true;
437         }
438         return false;
439     }
440 }
441