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