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.ComponentName;
20import android.content.Intent;
21import android.net.Uri;
22import android.telecom.Connection;
23import android.telecom.ConnectionRequest;
24import android.telecom.ConnectionService;
25import android.telecom.DisconnectCause;
26import android.telecom.PhoneAccount;
27import android.telecom.PhoneAccountHandle;
28import android.telephony.PhoneNumberUtils;
29import android.telephony.ServiceState;
30import android.telephony.TelephonyManager;
31import android.text.TextUtils;
32
33import com.android.internal.telephony.Call;
34import com.android.internal.telephony.CallStateException;
35import com.android.internal.telephony.Phone;
36import com.android.internal.telephony.PhoneConstants;
37import com.android.internal.telephony.PhoneFactory;
38import com.android.internal.telephony.PhoneProxy;
39import com.android.internal.telephony.SubscriptionController;
40import com.android.internal.telephony.cdma.CDMAPhone;
41import com.android.phone.MMIDialogActivity;
42
43import java.util.ArrayList;
44import java.util.List;
45import java.util.Objects;
46
47/**
48 * Service for making GSM and CDMA connections.
49 */
50public class TelephonyConnectionService extends ConnectionService {
51    private final GsmConferenceController mGsmConferenceController =
52            new GsmConferenceController(this);
53    private final CdmaConferenceController mCdmaConferenceController =
54            new CdmaConferenceController(this);
55    private ComponentName mExpectedComponentName = null;
56    private EmergencyCallHelper mEmergencyCallHelper;
57    private EmergencyTonePlayer mEmergencyTonePlayer;
58
59    @Override
60    public void onCreate() {
61        super.onCreate();
62        mExpectedComponentName = new ComponentName(this, this.getClass());
63        mEmergencyTonePlayer = new EmergencyTonePlayer(this);
64    }
65
66    @Override
67    public Connection onCreateOutgoingConnection(
68            PhoneAccountHandle connectionManagerPhoneAccount,
69            final ConnectionRequest request) {
70        Log.i(this, "onCreateOutgoingConnection, request: " + request);
71
72        Uri handle = request.getAddress();
73        if (handle == null) {
74            Log.d(this, "onCreateOutgoingConnection, handle is null");
75            return Connection.createFailedConnection(
76                    DisconnectCauseUtil.toTelecomDisconnectCause(
77                            android.telephony.DisconnectCause.NO_PHONE_NUMBER_SUPPLIED,
78                            "No phone number supplied"));
79        }
80
81        String scheme = handle.getScheme();
82        final String number;
83        if (PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) {
84            // TODO: We don't check for SecurityException here (requires
85            // CALL_PRIVILEGED permission).
86            final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
87            if (phone == null) {
88                Log.d(this, "onCreateOutgoingConnection, phone is null");
89                return Connection.createFailedConnection(
90                        DisconnectCauseUtil.toTelecomDisconnectCause(
91                                android.telephony.DisconnectCause.OUTGOING_FAILURE,
92                                "Phone is null"));
93            }
94            number = phone.getVoiceMailNumber();
95            if (TextUtils.isEmpty(number)) {
96                Log.d(this, "onCreateOutgoingConnection, no voicemail number set.");
97                return Connection.createFailedConnection(
98                        DisconnectCauseUtil.toTelecomDisconnectCause(
99                                android.telephony.DisconnectCause.VOICEMAIL_NUMBER_MISSING,
100                                "Voicemail scheme provided but no voicemail number set."));
101            }
102
103            // Convert voicemail: to tel:
104            handle = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
105        } else {
106            if (!PhoneAccount.SCHEME_TEL.equals(scheme)) {
107                Log.d(this, "onCreateOutgoingConnection, Handle %s is not type tel", scheme);
108                return Connection.createFailedConnection(
109                        DisconnectCauseUtil.toTelecomDisconnectCause(
110                                android.telephony.DisconnectCause.INVALID_NUMBER,
111                                "Handle scheme is not type tel"));
112            }
113
114            number = handle.getSchemeSpecificPart();
115            if (TextUtils.isEmpty(number)) {
116                Log.d(this, "onCreateOutgoingConnection, unable to parse number");
117                return Connection.createFailedConnection(
118                        DisconnectCauseUtil.toTelecomDisconnectCause(
119                                android.telephony.DisconnectCause.INVALID_NUMBER,
120                                "Unable to parse number"));
121            }
122        }
123
124        boolean isEmergencyNumber = PhoneNumberUtils.isPotentialEmergencyNumber(number);
125
126        // Get the right phone object from the account data passed in.
127        final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber);
128        if (phone == null) {
129            Log.d(this, "onCreateOutgoingConnection, phone is null");
130            return Connection.createFailedConnection(
131                    DisconnectCauseUtil.toTelecomDisconnectCause(
132                            android.telephony.DisconnectCause.OUTGOING_FAILURE, "Phone is null"));
133        }
134
135        int state = phone.getServiceState().getState();
136        boolean useEmergencyCallHelper = false;
137
138        if (isEmergencyNumber) {
139            if (state == ServiceState.STATE_POWER_OFF) {
140                useEmergencyCallHelper = true;
141            }
142        } else {
143            switch (state) {
144                case ServiceState.STATE_IN_SERVICE:
145                case ServiceState.STATE_EMERGENCY_ONLY:
146                    break;
147                case ServiceState.STATE_OUT_OF_SERVICE:
148                    return Connection.createFailedConnection(
149                            DisconnectCauseUtil.toTelecomDisconnectCause(
150                                    android.telephony.DisconnectCause.OUT_OF_SERVICE,
151                                    "ServiceState.STATE_OUT_OF_SERVICE"));
152                case ServiceState.STATE_POWER_OFF:
153                    return Connection.createFailedConnection(
154                            DisconnectCauseUtil.toTelecomDisconnectCause(
155                                    android.telephony.DisconnectCause.POWER_OFF,
156                                    "ServiceState.STATE_POWER_OFF"));
157                default:
158                    Log.d(this, "onCreateOutgoingConnection, unknown service state: %d", state);
159                    return Connection.createFailedConnection(
160                            DisconnectCauseUtil.toTelecomDisconnectCause(
161                                    android.telephony.DisconnectCause.OUTGOING_FAILURE,
162                                    "Unknown service state " + state));
163            }
164        }
165
166        final TelephonyConnection connection =
167                createConnectionFor(phone, null, true /* isOutgoing */);
168        if (connection == null) {
169            return Connection.createFailedConnection(
170                    DisconnectCauseUtil.toTelecomDisconnectCause(
171                            android.telephony.DisconnectCause.OUTGOING_FAILURE,
172                            "Invalid phone type"));
173        }
174        connection.setAddress(handle, PhoneConstants.PRESENTATION_ALLOWED);
175        connection.setInitializing();
176        connection.setVideoState(request.getVideoState());
177
178        if (useEmergencyCallHelper) {
179            if (mEmergencyCallHelper == null) {
180                mEmergencyCallHelper = new EmergencyCallHelper(this);
181            }
182            mEmergencyCallHelper.startTurnOnRadioSequence(phone,
183                    new EmergencyCallHelper.Callback() {
184                        @Override
185                        public void onComplete(boolean isRadioReady) {
186                            if (connection.getState() == Connection.STATE_DISCONNECTED) {
187                                // If the connection has already been disconnected, do nothing.
188                            } else if (isRadioReady) {
189                                connection.setInitialized();
190                                placeOutgoingConnection(connection, phone, request);
191                            } else {
192                                Log.d(this, "onCreateOutgoingConnection, failed to turn on radio");
193                                connection.setDisconnected(
194                                        DisconnectCauseUtil.toTelecomDisconnectCause(
195                                                android.telephony.DisconnectCause.POWER_OFF,
196                                                "Failed to turn on radio."));
197                                connection.destroy();
198                            }
199                        }
200                    });
201
202        } else {
203            placeOutgoingConnection(connection, phone, request);
204        }
205
206        return connection;
207    }
208
209    @Override
210    public Connection onCreateIncomingConnection(
211            PhoneAccountHandle connectionManagerPhoneAccount,
212            ConnectionRequest request) {
213        Log.i(this, "onCreateIncomingConnection, request: " + request);
214
215        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
216        if (phone == null) {
217            return Connection.createFailedConnection(
218                    DisconnectCauseUtil.toTelecomDisconnectCause(
219                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED));
220        }
221
222        Call call = phone.getRingingCall();
223        if (!call.getState().isRinging()) {
224            Log.i(this, "onCreateIncomingConnection, no ringing call");
225            return Connection.createFailedConnection(
226                    DisconnectCauseUtil.toTelecomDisconnectCause(
227                            android.telephony.DisconnectCause.INCOMING_MISSED,
228                            "Found no ringing call"));
229        }
230
231        com.android.internal.telephony.Connection originalConnection =
232                call.getState() == Call.State.WAITING ?
233                    call.getLatestConnection() : call.getEarliestConnection();
234        if (isOriginalConnectionKnown(originalConnection)) {
235            Log.i(this, "onCreateIncomingConnection, original connection already registered");
236            return Connection.createCanceledConnection();
237        }
238
239        Connection connection =
240                createConnectionFor(phone, originalConnection, false /* isOutgoing */);
241        if (connection == null) {
242            connection = Connection.createCanceledConnection();
243            return Connection.createCanceledConnection();
244        } else {
245            return connection;
246        }
247    }
248
249    @Override
250    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
251            ConnectionRequest request) {
252        Log.i(this, "onCreateUnknownConnection, request: " + request);
253
254        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
255        if (phone == null) {
256            return Connection.createFailedConnection(
257                    DisconnectCauseUtil.toTelecomDisconnectCause(
258                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED));
259        }
260
261        final List<com.android.internal.telephony.Connection> allConnections = new ArrayList<>();
262        final Call ringingCall = phone.getRingingCall();
263        if (ringingCall.hasConnections()) {
264            allConnections.addAll(ringingCall.getConnections());
265        }
266        final Call foregroundCall = phone.getForegroundCall();
267        if (foregroundCall.hasConnections()) {
268            allConnections.addAll(foregroundCall.getConnections());
269        }
270        final Call backgroundCall = phone.getBackgroundCall();
271        if (backgroundCall.hasConnections()) {
272            allConnections.addAll(phone.getBackgroundCall().getConnections());
273        }
274
275        com.android.internal.telephony.Connection unknownConnection = null;
276        for (com.android.internal.telephony.Connection telephonyConnection : allConnections) {
277            if (!isOriginalConnectionKnown(telephonyConnection)) {
278                unknownConnection = telephonyConnection;
279                break;
280            }
281        }
282
283        if (unknownConnection == null) {
284            Log.i(this, "onCreateUnknownConnection, did not find previously unknown connection.");
285            return Connection.createCanceledConnection();
286        }
287
288        TelephonyConnection connection =
289                createConnectionFor(phone, unknownConnection,
290                        !unknownConnection.isIncoming() /* isOutgoing */);
291
292        if (connection == null) {
293            return Connection.createCanceledConnection();
294        } else {
295            connection.updateState();
296            return connection;
297        }
298    }
299
300    @Override
301    public void onConference(Connection connection1, Connection connection2) {
302        if (connection1 instanceof TelephonyConnection &&
303                connection2 instanceof TelephonyConnection) {
304            ((TelephonyConnection) connection1).performConference(
305                (TelephonyConnection) connection2);
306        }
307
308    }
309
310    private void placeOutgoingConnection(
311            TelephonyConnection connection, Phone phone, ConnectionRequest request) {
312        String number = connection.getAddress().getSchemeSpecificPart();
313
314        com.android.internal.telephony.Connection originalConnection;
315        try {
316            originalConnection = phone.dial(number, request.getVideoState());
317        } catch (CallStateException e) {
318            Log.e(this, e, "placeOutgoingConnection, phone.dial exception: " + e);
319            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
320                    android.telephony.DisconnectCause.OUTGOING_FAILURE,
321                    e.getMessage()));
322            return;
323        }
324
325        if (originalConnection == null) {
326            int telephonyDisconnectCause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
327            // On GSM phones, null connection means that we dialed an MMI code
328            if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
329                Log.d(this, "dialed MMI code");
330                telephonyDisconnectCause = android.telephony.DisconnectCause.DIALED_MMI;
331                final Intent intent = new Intent(this, MMIDialogActivity.class);
332                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
333                        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
334                startActivity(intent);
335            }
336            Log.d(this, "placeOutgoingConnection, phone.dial returned null");
337            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
338                    telephonyDisconnectCause, "Connection is null"));
339        } else {
340            connection.setOriginalConnection(originalConnection);
341        }
342    }
343
344    private TelephonyConnection createConnectionFor(
345            Phone phone,
346            com.android.internal.telephony.Connection originalConnection,
347            boolean isOutgoing) {
348        int phoneType = phone.getPhoneType();
349        if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
350            GsmConnection connection = new GsmConnection(originalConnection);
351            mGsmConferenceController.add(connection);
352            return connection;
353        } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
354            boolean allowMute = allowMute(phone);
355            CdmaConnection connection = new CdmaConnection(
356                    originalConnection, mEmergencyTonePlayer, allowMute, isOutgoing);
357            mCdmaConferenceController.add(connection);
358            return connection;
359        } else {
360            return null;
361        }
362    }
363
364    private boolean isOriginalConnectionKnown(
365            com.android.internal.telephony.Connection originalConnection) {
366        for (Connection connection : getAllConnections()) {
367            TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
368            if (connection instanceof TelephonyConnection) {
369                if (telephonyConnection.getOriginalConnection() == originalConnection) {
370                    return true;
371                }
372            }
373        }
374        return false;
375    }
376
377    private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency) {
378        if (isEmergency) {
379            return PhoneFactory.getDefaultPhone();
380        }
381
382        if (Objects.equals(mExpectedComponentName, accountHandle.getComponentName())) {
383            if (accountHandle.getId() != null) {
384                try {
385                    int phoneId = SubscriptionController.getInstance().getPhoneId(
386                            Long.parseLong(accountHandle.getId()));
387                    return PhoneFactory.getPhone(phoneId);
388                } catch (NumberFormatException e) {
389                    Log.w(this, "Could not get subId from account: " + accountHandle.getId());
390                }
391            }
392        }
393        return null;
394    }
395
396    /**
397     * Determines if the connection should allow mute.
398     *
399     * @param phone The current phone.
400     * @return {@code True} if the connection should allow mute.
401     */
402    private boolean allowMute(Phone phone) {
403        // For CDMA phones, check if we are in Emergency Callback Mode (ECM).  Mute is disallowed
404        // in ECM mode.
405        if (phone.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
406            PhoneProxy phoneProxy = (PhoneProxy)phone;
407            CDMAPhone cdmaPhone = (CDMAPhone)phoneProxy.getActivePhone();
408            if (cdmaPhone != null) {
409                if (cdmaPhone.isInEcm()) {
410                    return false;
411                }
412            }
413        }
414
415        return true;
416    }
417}
418