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
17package com.android.services.telephony;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.net.Uri;
24import android.os.AsyncResult;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.Message;
28import android.os.SystemClock;
29import android.telecom.PhoneAccount;
30import android.telecom.PhoneAccountHandle;
31import android.telecom.TelecomManager;
32import android.text.TextUtils;
33
34import com.android.internal.telephony.Call;
35import com.android.internal.telephony.CallStateException;
36import com.android.internal.telephony.Connection;
37import com.android.internal.telephony.DriverCall;
38import com.android.internal.telephony.GsmCdmaCallTracker;
39import com.android.internal.telephony.GsmCdmaConnection;
40import com.android.internal.telephony.GsmCdmaPhone;
41import com.android.internal.telephony.Phone;
42import com.android.internal.telephony.TelephonyComponentFactory;
43import com.android.internal.telephony.TelephonyIntents;
44import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
45import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
46import com.android.internal.telephony.imsphone.ImsExternalConnection;
47import com.android.phone.PhoneUtils;
48
49import com.google.common.base.Preconditions;
50
51import 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 */
57final 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