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.telecom.PhoneAccount;
29import android.telecom.TelecomManager;
30import android.telephony.TelephonyManager;
31import android.text.TextUtils;
32
33import com.android.internal.telephony.Call;
34import com.android.internal.telephony.Connection;
35import com.android.internal.telephony.Phone;
36import com.android.internal.telephony.PhoneConstants;
37import com.android.internal.telephony.PhoneProxy;
38import com.android.internal.telephony.TelephonyIntents;
39import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
40import com.android.phone.PhoneUtils;
41
42import com.google.common.base.Preconditions;
43
44import 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 */
50final 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 proxy object to listen to. */
57    private final PhoneProxy mPhoneProxy;
58
59    /**
60     * The base phone implementation behind phone proxy. The underlying phone implementation can
61     * change underneath when the radio technology changes. We listen for these events and update
62     * the base phone in this variable. We save it so that when the change happens, we can
63     * unregister from the events we were listening to.
64     */
65    private Phone mPhoneBase;
66
67    /**
68     * Used to listen to events from {@link #mPhoneBase}.
69     */
70    private final Handler mHandler = new Handler() {
71        @Override
72        public void handleMessage(Message msg) {
73            switch(msg.what) {
74                case EVENT_NEW_RINGING_CONNECTION:
75                    handleNewRingingConnection((AsyncResult) msg.obj);
76                    break;
77                case EVENT_CDMA_CALL_WAITING:
78                    handleCdmaCallWaiting((AsyncResult) msg.obj);
79                    break;
80                case EVENT_UNKNOWN_CONNECTION:
81                    handleNewUnknownConnection((AsyncResult) msg.obj);
82                    break;
83                default:
84                    break;
85            }
86        }
87    };
88
89    /**
90     * Receiver to listen for radio technology change events.
91     */
92    private final BroadcastReceiver mRATReceiver = new BroadcastReceiver() {
93        @Override
94        public void onReceive(Context context, Intent intent) {
95            String action = intent.getAction();
96            if (TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED.equals(action)) {
97                String newPhone = intent.getStringExtra(PhoneConstants.PHONE_NAME_KEY);
98                Log.d(this, "Radio technology switched. Now %s is active.", newPhone);
99
100                registerForNotifications();
101            }
102        }
103    };
104
105    /**
106     * Persists the specified parameters and starts listening to phone events.
107     *
108     * @param phoneProxy The phone object for listening to incoming calls.
109     */
110    PstnIncomingCallNotifier(PhoneProxy phoneProxy) {
111        Preconditions.checkNotNull(phoneProxy);
112
113        mPhoneProxy = phoneProxy;
114
115        registerForNotifications();
116
117        IntentFilter intentFilter =
118                new IntentFilter(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED);
119        mPhoneProxy.getContext().registerReceiver(mRATReceiver, intentFilter);
120    }
121
122    void teardown() {
123        unregisterForNotifications();
124        mPhoneProxy.getContext().unregisterReceiver(mRATReceiver);
125    }
126
127    /**
128     * Register for notifications from the base phone.
129     * TODO: We should only need to interact with the phoneproxy directly. However,
130     * since the phoneproxy only interacts directly with CallManager we either listen to callmanager
131     * or we have to poke into the proxy like this.  Neither is desirable. It would be better if
132     * this class and callManager could register generically with the phone proxy instead and get
133     * radio techonology changes directly.  Or better yet, just register for the notifications
134     * directly with phone proxy and never worry about the technology changes. This requires a
135     * change in opt/telephony code.
136     */
137    private void registerForNotifications() {
138        Phone newPhone = mPhoneProxy.getActivePhone();
139        if (newPhone != mPhoneBase) {
140            unregisterForNotifications();
141
142            if (newPhone != null) {
143                Log.i(this, "Registering: %s", newPhone);
144                mPhoneBase = newPhone;
145                mPhoneBase.registerForNewRingingConnection(
146                        mHandler, EVENT_NEW_RINGING_CONNECTION, null);
147                mPhoneBase.registerForCallWaiting(
148                        mHandler, EVENT_CDMA_CALL_WAITING, null);
149                mPhoneBase.registerForUnknownConnection(mHandler, EVENT_UNKNOWN_CONNECTION,
150                        null);
151            }
152        }
153    }
154
155    private void unregisterForNotifications() {
156        if (mPhoneBase != null) {
157            Log.i(this, "Unregistering: %s", mPhoneBase);
158            mPhoneBase.unregisterForNewRingingConnection(mHandler);
159            mPhoneBase.unregisterForCallWaiting(mHandler);
160            mPhoneBase.unregisterForUnknownConnection(mHandler);
161        }
162    }
163
164    /**
165     * Verifies the incoming call and triggers sending the incoming-call intent to Telecom.
166     *
167     * @param asyncResult The result object from the new ringing event.
168     */
169    private void handleNewRingingConnection(AsyncResult asyncResult) {
170        Log.d(this, "handleNewRingingConnection");
171        Connection connection = (Connection) asyncResult.result;
172        if (connection != null) {
173            Call call = connection.getCall();
174
175            // Final verification of the ringing state before sending the intent to Telecom.
176            if (call != null && call.getState().isRinging()) {
177                sendIncomingCallIntent(connection);
178            }
179        }
180    }
181
182    private void handleCdmaCallWaiting(AsyncResult asyncResult) {
183        Log.d(this, "handleCdmaCallWaiting");
184        CdmaCallWaitingNotification ccwi = (CdmaCallWaitingNotification) asyncResult.result;
185        Call call = mPhoneBase.getRingingCall();
186        if (call.getState() == Call.State.WAITING) {
187            Connection connection = call.getLatestConnection();
188            if (connection != null) {
189                String number = connection.getAddress();
190                if (number != null && Objects.equals(number, ccwi.number)) {
191                    sendIncomingCallIntent(connection);
192                }
193            }
194        }
195    }
196
197    private void handleNewUnknownConnection(AsyncResult asyncResult) {
198        Log.i(this, "handleNewUnknownConnection");
199        if (!(asyncResult.result instanceof Connection)) {
200            Log.w(this, "handleNewUnknownConnection called with non-Connection object");
201            return;
202        }
203        Connection connection = (Connection) asyncResult.result;
204        if (connection != null) {
205            Call call = connection.getCall();
206            if (call != null && call.getState().isAlive()) {
207                addNewUnknownCall(connection);
208            }
209        }
210    }
211
212    private void addNewUnknownCall(Connection connection) {
213        Log.i(this, "addNewUnknownCall, connection is: %s", connection);
214        if (!maybeSwapAnyWithUnknownConnection(connection)) {
215            Log.i(this, "determined new connection is: %s", connection);
216            Bundle extras = null;
217            if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
218                    !TextUtils.isEmpty(connection.getAddress())) {
219                extras = new Bundle();
220                Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null);
221                extras.putParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE, uri);
222            }
223            TelecomManager.from(mPhoneProxy.getContext()).addNewUnknownCall(
224                    PhoneUtils.makePstnPhoneAccountHandle(mPhoneProxy), extras);
225        } else {
226            Log.i(this, "swapped an old connection, new one is: %s", connection);
227        }
228    }
229
230    /**
231     * Sends the incoming call intent to telecom.
232     */
233    private void sendIncomingCallIntent(Connection connection) {
234        Bundle extras = null;
235        if (connection.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
236                !TextUtils.isEmpty(connection.getAddress())) {
237            extras = new Bundle();
238            Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, connection.getAddress(), null);
239            extras.putParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER, uri);
240        }
241        TelecomManager.from(mPhoneProxy.getContext()).addNewIncomingCall(
242                PhoneUtils.makePstnPhoneAccountHandle(mPhoneProxy), extras);
243    }
244
245    /**
246     * Define cait.Connection := com.android.internal.telephony.Connection
247     *
248     * Given a previously unknown cait.Connection, check to see if it's likely a replacement for
249     * another cait.Connnection we already know about. If it is, then we silently swap it out
250     * underneath within the relevant {@link TelephonyConnection}, using
251     * {@link TelephonyConnection#setOriginalConnection(Connection)}, and return {@code true}.
252     * Otherwise, we return {@code false}.
253     */
254    private boolean maybeSwapAnyWithUnknownConnection(Connection unknown) {
255        if (!unknown.isIncoming()) {
256            TelecomAccountRegistry registry = TelecomAccountRegistry.getInstance(null);
257            if (registry != null) {
258                TelephonyConnectionService service = registry.getTelephonyConnectionService();
259                if (service != null) {
260                    for (android.telecom.Connection telephonyConnection : service
261                            .getAllConnections()) {
262                        if (maybeSwapWithUnknownConnection(
263                                (TelephonyConnection) telephonyConnection,
264                                unknown)) {
265                            return true;
266                        }
267                    }
268                }
269            }
270        }
271        return false;
272    }
273
274    private boolean maybeSwapWithUnknownConnection(
275            TelephonyConnection telephonyConnection,
276            Connection unknown) {
277        Connection original = telephonyConnection.getOriginalConnection();
278        if (original != null && !original.isIncoming()
279                && Objects.equals(original.getAddress(), unknown.getAddress())) {
280            telephonyConnection.setOriginalConnection(unknown);
281            return true;
282        }
283        return false;
284    }
285}
286