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.sip;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.net.ConnectivityManager;
23 import android.net.NetworkInfo;
24 import android.net.sip.SipAudioCall;
25 import android.net.sip.SipException;
26 import android.net.sip.SipManager;
27 import android.net.sip.SipProfile;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.telecom.Connection;
31 import android.telecom.ConnectionRequest;
32 import android.telecom.ConnectionService;
33 import android.telecom.PhoneAccountHandle;
34 import android.telecom.TelecomManager;
35 import android.telephony.DisconnectCause;
36 import android.util.Log;
37 
38 import com.android.internal.telephony.CallStateException;
39 import com.android.internal.telephony.PhoneFactory;
40 import com.android.internal.telephony.sip.SipPhone;
41 import com.android.services.telephony.DisconnectCauseUtil;
42 
43 import java.util.List;
44 import java.util.Objects;
45 
46 public final class SipConnectionService extends ConnectionService {
47     private interface IProfileFinderCallback {
onFound(SipProfile profile)48         void onFound(SipProfile profile);
49     }
50 
51     private static final String PREFIX = "[SipConnectionService] ";
52     private static final boolean VERBOSE = false; /* STOP SHIP if true */
53 
54     private SipProfileDb mSipProfileDb;
55     private Handler mHandler;
56 
57     @Override
onCreate()58     public void onCreate() {
59         mSipProfileDb = new SipProfileDb(this);
60         mHandler = new Handler();
61         super.onCreate();
62     }
63 
64     @Override
onCreateOutgoingConnection( PhoneAccountHandle connectionManagerAccount, final ConnectionRequest request)65     public Connection onCreateOutgoingConnection(
66             PhoneAccountHandle connectionManagerAccount,
67             final ConnectionRequest request) {
68         if (VERBOSE) log("onCreateOutgoingConnection, request: " + request);
69 
70         Bundle extras = request.getExtras();
71         if (extras != null &&
72                 extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE) != null) {
73             return Connection.createFailedConnection(DisconnectCauseUtil.toTelecomDisconnectCause(
74                     DisconnectCause.CALL_BARRED, "Cannot make a SIP call with a gateway number."));
75         }
76 
77         PhoneAccountHandle accountHandle = request.getAccountHandle();
78         ComponentName sipComponentName = new ComponentName(this, SipConnectionService.class);
79         if (!Objects.equals(accountHandle.getComponentName(), sipComponentName)) {
80             return Connection.createFailedConnection(DisconnectCauseUtil.toTelecomDisconnectCause(
81                     DisconnectCause.OUTGOING_FAILURE, "Did not match service connection"));
82         }
83 
84         final SipConnection connection = new SipConnection();
85         connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
86         connection.setInitializing();
87         connection.onAddedToCallService();
88         boolean attemptCall = true;
89 
90         if (!SipUtil.isVoipSupported(this)) {
91             final CharSequence description = getString(R.string.no_voip);
92             connection.setDisconnected(new android.telecom.DisconnectCause(
93                     android.telecom.DisconnectCause.ERROR, null, description,
94                     "VoIP unsupported"));
95             attemptCall = false;
96         }
97 
98         if (attemptCall && !isNetworkConnected()) {
99             if (VERBOSE) log("start, network not connected, dropping call");
100             final boolean wifiOnly = SipManager.isSipWifiOnly(this);
101             final CharSequence description = getString(wifiOnly ? R.string.no_wifi_available
102                     : R.string.no_internet_available);
103             connection.setDisconnected(new android.telecom.DisconnectCause(
104                     android.telecom.DisconnectCause.ERROR, null, description,
105                     "Network not connected"));
106             attemptCall = false;
107         }
108 
109         if (attemptCall) {
110             // The ID used for SIP-based phone account is the SIP profile Uri. Use it to find
111             // the actual profile.
112             String profileName = accountHandle.getId();
113             findProfile(profileName, new IProfileFinderCallback() {
114                 @Override
115                 public void onFound(SipProfile profile) {
116                     if (profile == null) {
117                         connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
118                                 DisconnectCause.OUTGOING_FAILURE, "SIP profile not found."));
119                         connection.destroy();
120                     } else {
121                         com.android.internal.telephony.Connection chosenConnection =
122                                 createConnectionForProfile(profile, request);
123                         if (chosenConnection == null) {
124                             connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
125                                     DisconnectCause.OUTGOING_FAILURE, "Connection failed."));
126                             connection.destroy();
127                         } else {
128                             if (VERBOSE) log("initializing connection");
129                             connection.initialize(chosenConnection);
130                         }
131                     }
132                 }
133             });
134         }
135 
136         return connection;
137     }
138 
139     @Override
onCreateIncomingConnection( PhoneAccountHandle connectionManagerAccount, ConnectionRequest request)140     public Connection onCreateIncomingConnection(
141             PhoneAccountHandle connectionManagerAccount,
142             ConnectionRequest request) {
143         if (VERBOSE) log("onCreateIncomingConnection, request: " + request);
144 
145         if (request.getExtras() == null) {
146             if (VERBOSE) log("onCreateIncomingConnection, no extras");
147             return Connection.createFailedConnection(DisconnectCauseUtil.toTelecomDisconnectCause(
148                     DisconnectCause.ERROR_UNSPECIFIED, "No extras on request."));
149         }
150 
151         Intent sipIntent = (Intent) request.getExtras().getParcelable(
152                 SipUtil.EXTRA_INCOMING_CALL_INTENT);
153         if (sipIntent == null) {
154             if (VERBOSE) log("onCreateIncomingConnection, no SIP intent");
155             return Connection.createFailedConnection(DisconnectCauseUtil.toTelecomDisconnectCause(
156                     DisconnectCause.ERROR_UNSPECIFIED, "No SIP intent."));
157         }
158 
159         SipAudioCall sipAudioCall;
160         try {
161             sipAudioCall = SipManager.newInstance(this).takeAudioCall(sipIntent, null);
162         } catch (SipException e) {
163             log("onCreateIncomingConnection, takeAudioCall exception: " + e);
164             return Connection.createCanceledConnection();
165         }
166 
167         SipPhone phone = findPhoneForProfile(sipAudioCall.getLocalProfile());
168         if (phone == null) {
169             phone = createPhoneForProfile(sipAudioCall.getLocalProfile());
170         }
171         if (phone != null) {
172             com.android.internal.telephony.Connection originalConnection = phone.takeIncomingCall(
173                     sipAudioCall);
174             if (VERBOSE) log("onCreateIncomingConnection, new connection: " + originalConnection);
175             if (originalConnection != null) {
176                 SipConnection sipConnection = new SipConnection();
177                 sipConnection.initialize(originalConnection);
178                 sipConnection.onAddedToCallService();
179                 return sipConnection;
180             } else {
181                 if (VERBOSE) log("onCreateIncomingConnection, takingIncomingCall failed");
182                 return Connection.createCanceledConnection();
183             }
184         }
185         return Connection.createFailedConnection(DisconnectCauseUtil.toTelecomDisconnectCause(
186                 DisconnectCause.ERROR_UNSPECIFIED));
187     }
188 
createConnectionForProfile( SipProfile profile, ConnectionRequest request)189     private com.android.internal.telephony.Connection createConnectionForProfile(
190             SipProfile profile,
191             ConnectionRequest request) {
192         SipPhone phone = findPhoneForProfile(profile);
193         if (phone == null) {
194             phone = createPhoneForProfile(profile);
195         }
196         if (phone != null) {
197             return startCallWithPhone(phone, request);
198         }
199         return null;
200     }
201 
202     /**
203      * Searched for the specified profile in the SIP profile database.  This can take a long time
204      * in communicating with the database, so it is done asynchronously with a separate thread and a
205      * callback interface.
206      */
findProfile(final String profileName, final IProfileFinderCallback callback)207     private void findProfile(final String profileName, final IProfileFinderCallback callback) {
208         if (VERBOSE) log("findProfile");
209         new Thread(new Runnable() {
210             @Override
211             public void run() {
212                 SipProfile profileToUse = null;
213                 List<SipProfile> profileList = mSipProfileDb.retrieveSipProfileList();
214                 if (profileList != null) {
215                     for (SipProfile profile : profileList) {
216                         if (Objects.equals(profileName, profile.getProfileName())) {
217                             profileToUse = profile;
218                             break;
219                         }
220                     }
221                 }
222 
223                 final SipProfile profileFound = profileToUse;
224                 mHandler.post(new Runnable() {
225                     @Override
226                     public void run() {
227                         callback.onFound(profileFound);
228                     }
229                 });
230             }
231         }).start();
232     }
233 
findPhoneForProfile(SipProfile profile)234     private SipPhone findPhoneForProfile(SipProfile profile) {
235         if (VERBOSE) log("findPhoneForProfile, profile: " + profile);
236         for (Connection connection : getAllConnections()) {
237             if (connection instanceof SipConnection) {
238                 SipPhone phone = ((SipConnection) connection).getPhone();
239                 if (phone != null && phone.getSipUri().equals(profile.getUriString())) {
240                     if (VERBOSE) log("findPhoneForProfile, found existing phone: " + phone);
241                     return phone;
242                 }
243             }
244         }
245         if (VERBOSE) log("findPhoneForProfile, no phone found");
246         return null;
247     }
248 
createPhoneForProfile(SipProfile profile)249     private SipPhone createPhoneForProfile(SipProfile profile) {
250         if (VERBOSE) log("createPhoneForProfile, profile: " + profile);
251         return PhoneFactory.makeSipPhone(profile.getUriString());
252     }
253 
startCallWithPhone( SipPhone phone, ConnectionRequest request)254     private com.android.internal.telephony.Connection startCallWithPhone(
255             SipPhone phone, ConnectionRequest request) {
256         String number = request.getAddress().getSchemeSpecificPart();
257         if (VERBOSE) log("startCallWithPhone, number: " + number);
258 
259         try {
260             com.android.internal.telephony.Connection originalConnection =
261                     phone.dial(number, request.getVideoState());
262             return originalConnection;
263         } catch (CallStateException e) {
264             log("startCallWithPhone, exception: " + e);
265             return null;
266         }
267     }
268 
isNetworkConnected()269     private boolean isNetworkConnected() {
270         ConnectivityManager cm =
271                 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
272         if (cm != null) {
273             NetworkInfo ni = cm.getActiveNetworkInfo();
274             if (ni != null && ni.isConnected()) {
275                 return ni.getType() == ConnectivityManager.TYPE_WIFI ||
276                         !SipManager.isSipWifiOnly(this);
277             }
278         }
279         return false;
280     }
281 
log(String msg)282     private static void log(String msg) {
283         Log.d(SipUtil.LOG_TAG, PREFIX + msg);
284     }
285 }
286