Merge "Add hidden systemapi hasCarrierPrivileges(pkgname) to PhoneInterfaceManager."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4279616..f54b63a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -466,9 +466,16 @@
</intent-filter>
</activity>
- <service android:singleUser="true"
- android:name="com.android.services.telephony.sip.SipConnectionService"
- android:label="@string/sip_connection_service_label">
+ <!-- Start SIP -->
+ <service android:name="com.android.services.telephony.sip.SipCallServiceProvider"
+ android:singleUser="true" >
+ <intent-filter>
+ <action android:name="android.telecomm.CallServiceProvider" />
+ </intent-filter>
+ </service>
+ <service android:name="com.android.services.telephony.sip.SipConnectionService"
+ android:label="@string/sip_connection_service_label"
+ android:singleUser="true" >
<intent-filter>
<action android:name="android.telecomm.ConnectionService" />
</intent-filter>
@@ -497,13 +504,15 @@
android:configChanges="orientation|screenSize|keyboardHidden"
android:uiOptions="splitActionBarWhenNarrow">
</activity>
- <activity android:name="com.android.services.telephony.SipCallOptionHandler"
+ <activity android:name="com.android.services.telephony.sip.SipProfileChooserDialogs"
android:theme="@style/SipCallOptionHandlerTheme"
android:screenOrientation="nosensor"
android:configChanges="orientation|screenSize|keyboardHidden"
android:excludeFromRecents="true">
</activity>
+ <!-- End SIP -->
+
<activity android:name="ErrorDialogActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:excludeFromRecents="true"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0e0042b..aa60664 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1306,15 +1306,6 @@
Do not translate. -->
<string name="call_settings_title_font_family">sans-serif-light</string>
- <!-- Title of the dialog to choose WiFi calling. -->
- <string name ="choose_wifi_for_call_msg">Use Wi-Fi to place call?</string>
-
- <!-- Title for button to use WiFi calling. -->
- <string name ="choose_wifi_for_call_yes">Yes</string>
-
- <!-- Title for button to not use WiFi calling. -->
- <string name ="choose_wifi_for_call_no">No</string>
-
<!-- Label for PSTN connection service. -->
<string name="pstn_connection_service_label">Built-in SIM cards</string>
diff --git a/sip/src/com/android/services/telephony/sip/SipBroadcastReceiver.java b/sip/src/com/android/services/telephony/sip/SipBroadcastReceiver.java
index b949351..62b8266 100644
--- a/sip/src/com/android/services/telephony/sip/SipBroadcastReceiver.java
+++ b/sip/src/com/android/services/telephony/sip/SipBroadcastReceiver.java
@@ -16,18 +16,15 @@
package com.android.services.telephony.sip;
-import com.android.internal.telephony.Phone;
-import com.android.internal.telephony.PhoneConstants;
-import com.android.internal.telephony.PhoneFactory;
-import com.android.internal.telephony.sip.SipPhone;
-
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.net.sip.SipAudioCall;
import android.net.sip.SipException;
import android.net.sip.SipManager;
import android.net.sip.SipProfile;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.telecomm.TelecommConstants;
import android.util.Log;
import java.util.List;
@@ -59,7 +56,17 @@
private void takeCall(Context context, Intent intent) {
if (VERBOSE) log("takeCall, intent: " + intent);
- // TODO(sail): Add support for incoming SIP calls.
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(SipUtil.EXTRA_INCOMING_CALL_INTENT, intent);
+
+ Intent telecommIntent = new Intent(TelecommConstants.ACTION_INCOMING_CALL);
+ telecommIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ telecommIntent.putExtra(TelecommConstants.EXTRA_CALL_SERVICE_DESCRIPTOR,
+ SipCallServiceProvider.getDescriptor(context));
+ telecommIntent.putExtra(TelecommConstants.EXTRA_INCOMING_CALL_EXTRAS, extras);
+
+ context.startActivityAsUser(telecommIntent, UserHandle.CURRENT);
}
private void registerAllProfiles(final Context context) {
diff --git a/sip/src/com/android/services/telephony/sip/SipCallServiceProvider.java b/sip/src/com/android/services/telephony/sip/SipCallServiceProvider.java
new file mode 100644
index 0000000..cc467e9
--- /dev/null
+++ b/sip/src/com/android/services/telephony/sip/SipCallServiceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.sip;
+
+import android.content.Context;
+import android.telecomm.CallServiceDescriptor;
+import android.telecomm.CallServiceLookupResponse;
+import android.telecomm.CallServiceProvider;
+
+import java.util.ArrayList;
+
+public class SipCallServiceProvider extends CallServiceProvider {
+ @Override
+ public void lookupCallServices(CallServiceLookupResponse response) {
+ ArrayList<CallServiceDescriptor> descriptors = new ArrayList<CallServiceDescriptor>();
+ descriptors.add(getDescriptor(this));
+ response.setCallServiceDescriptors(descriptors);
+ }
+
+ static CallServiceDescriptor getDescriptor(Context context) {
+ return CallServiceDescriptor.newBuilder(context)
+ .setConnectionService(SipConnectionService.class)
+ .setNetworkType(CallServiceDescriptor.FLAG_WIFI | CallServiceDescriptor.FLAG_MOBILE)
+ .build();
+ }
+}
diff --git a/sip/src/com/android/services/telephony/sip/SipConnection.java b/sip/src/com/android/services/telephony/sip/SipConnection.java
index 40239a4..c6e1be6 100644
--- a/sip/src/com/android/services/telephony/sip/SipConnection.java
+++ b/sip/src/com/android/services/telephony/sip/SipConnection.java
@@ -16,26 +16,54 @@
package com.android.services.telephony.sip;
+import android.os.Handler;
+import android.os.Message;
import android.telecomm.CallAudioState;
+import android.telecomm.CallCapabilities;
import android.telecomm.Connection;
import android.util.Log;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.sip.SipPhone;
+
import java.util.List;
-public class SipConnection extends Connection {
+final class SipConnection extends Connection {
private static final String PREFIX = "[SipConnection] ";
private static final boolean VERBOSE = true; /* STOP SHIP if true */
- private final com.android.internal.telephony.Connection mConnection;
+ private static final int MSG_PRECISE_CALL_STATE_CHANGED = 1;
- public SipConnection(com.android.internal.telephony.Connection connection) {
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PRECISE_CALL_STATE_CHANGED:
+ updateState();
+ break;
+ }
+ }
+ };
+
+ private com.android.internal.telephony.Connection mOriginalConnection;
+ private Call.State mOriginalConnectionState = Call.State.IDLE;
+
+ SipConnection(com.android.internal.telephony.Connection connection) {
if (VERBOSE) log("new SipConnection, connection: " + connection);
- mConnection = connection;
+ mOriginalConnection = connection;
+ if (getPhone() != null) {
+ getPhone().registerForPreciseCallStateChanged(mHandler, MSG_PRECISE_CALL_STATE_CHANGED,
+ null);
+ }
}
@Override
protected void onSetAudioState(CallAudioState state) {
if (VERBOSE) log("onSetAudioState: " + state);
+ if (getPhone() != null) {
+ getPhone().setEchoSuppressionEnabled();
+ }
}
@Override
@@ -46,51 +74,103 @@
@Override
protected void onPlayDtmfTone(char c) {
if (VERBOSE) log("onPlayDtmfTone");
+ if (getPhone() != null) {
+ getPhone().startDtmf(c);
+ }
}
@Override
protected void onStopDtmfTone() {
if (VERBOSE) log("onStopDtmfTone");
+ if (getPhone() != null) {
+ getPhone().stopDtmf();
+ }
}
@Override
protected void onDisconnect() {
if (VERBOSE) log("onDisconnect");
+ try {
+ if (getCall() != null && !getCall().isMultiparty()) {
+ getCall().hangup();
+ } else if (mOriginalConnection != null) {
+ mOriginalConnection.hangup();
+ }
+ } catch (CallStateException e) {
+ log("onDisconnect, exception: " + e);
+ }
}
@Override
protected void onSeparate() {
if (VERBOSE) log("onSeparate");
+ try {
+ if (mOriginalConnection != null) {
+ mOriginalConnection.separate();
+ }
+ } catch (CallStateException e) {
+ log("onSeparate, exception: " + e);
+ }
}
@Override
protected void onAbort() {
if (VERBOSE) log("onAbort");
+ onDisconnect();
}
@Override
protected void onHold() {
if (VERBOSE) log("onHold");
+ try {
+ if (getPhone() != null && getState() == State.ACTIVE) {
+ getPhone().switchHoldingAndActive();
+ }
+ } catch (CallStateException e) {
+ log("onHold, exception: " + e);
+ }
}
@Override
protected void onUnhold() {
if (VERBOSE) log("onUnhold");
+ try {
+ if (getPhone() != null && getState() == State.HOLDING) {
+ getPhone().switchHoldingAndActive();
+ }
+ } catch (CallStateException e) {
+ log("onUnhold, exception: " + e);
+ }
}
@Override
protected void onAnswer() {
if (VERBOSE) log("onAnswer");
+ try {
+ if (isValidRingingCall() && getPhone() != null) {
+ getPhone().acceptCall();
+ }
+ } catch (CallStateException e) {
+ log("onAnswer, exception: " + e);
+ }
}
@Override
protected void onReject() {
if (VERBOSE) log("onReject");
+ try {
+ if (isValidRingingCall() && getPhone() != null) {
+ getPhone().rejectCall();
+ }
+ } catch (CallStateException e) {
+ log("onReject, exception: " + e);
+ }
}
@Override
protected void onPostDialContinue(boolean proceed) {
if (VERBOSE) log("onPostDialContinue, proceed: " + proceed);
+ // SIP doesn't have post dial support.
}
@Override
@@ -103,6 +183,93 @@
if (VERBOSE) log("onPhoneAccountClicked");
}
+ private Call getCall() {
+ if (mOriginalConnection != null) {
+ return mOriginalConnection.getCall();
+ }
+ return null;
+ }
+
+ SipPhone getPhone() {
+ Call call = getCall();
+ if (call != null) {
+ return (SipPhone) call.getPhone();
+ }
+ return null;
+ }
+
+ private boolean isValidRingingCall() {
+ Call call = getCall();
+ return call != null && call.getState().isRinging() &&
+ call.getEarliestConnection() == mOriginalConnection;
+ }
+
+ private void updateState() {
+ if (mOriginalConnection == null) {
+ return;
+ }
+
+ Call.State newState = mOriginalConnection.getState();
+ if (VERBOSE) log("updateState, " + mOriginalConnectionState + " -> " + newState);
+ if (mOriginalConnectionState != newState) {
+ mOriginalConnectionState = newState;
+ switch (newState) {
+ case IDLE:
+ break;
+ case ACTIVE:
+ setActive();
+ break;
+ case HOLDING:
+ setOnHold();
+ break;
+ case DIALING:
+ case ALERTING:
+ setDialing();
+ break;
+ case INCOMING:
+ case WAITING:
+ setRinging();
+ break;
+ case DISCONNECTED:
+ setDisconnected(mOriginalConnection.getDisconnectCause(), null);
+ close();
+ break;
+ case DISCONNECTING:
+ break;
+ }
+ updateCallCapabilities();
+ }
+ }
+
+ private int buildCallCapabilities() {
+ int capabilities = CallCapabilities.MUTE | CallCapabilities.SUPPORT_HOLD;
+ if (getState() == State.ACTIVE || getState() == State.HOLDING) {
+ capabilities |= CallCapabilities.HOLD;
+ }
+ return capabilities;
+ }
+
+ void updateCallCapabilities() {
+ int newCallCapabilities = buildCallCapabilities();
+ if (getCallCapabilities() != newCallCapabilities) {
+ setCallCapabilities(newCallCapabilities);
+ }
+ }
+
+ void onAddedToCallService() {
+ if (VERBOSE) log("onAddedToCallService");
+ updateCallCapabilities();
+ setAudioModeIsVoip(true);
+ }
+
+ private void close() {
+ if (getPhone() != null) {
+ getPhone().unregisterForPreciseCallStateChanged(mHandler);
+ }
+ mOriginalConnection = null;
+ setDestroyed();
+ }
+
private static void log(String msg) {
Log.d(SipUtil.LOG_TAG, PREFIX + msg);
}
diff --git a/sip/src/com/android/services/telephony/sip/SipConnectionService.java b/sip/src/com/android/services/telephony/sip/SipConnectionService.java
index 9b7b33f..1dc78e3 100644
--- a/sip/src/com/android/services/telephony/sip/SipConnectionService.java
+++ b/sip/src/com/android/services/telephony/sip/SipConnectionService.java
@@ -17,6 +17,8 @@
package com.android.services.telephony.sip;
import android.content.Context;
+import android.content.Intent;
+import android.net.sip.SipAudioCall;
import android.net.sip.SipException;
import android.net.sip.SipManager;
import android.net.sip.SipProfile;
@@ -32,76 +34,106 @@
import android.util.Log;
import com.android.internal.telephony.CallStateException;
-import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.sip.SipPhone;
import java.util.HashMap;
-public class SipConnectionService extends ConnectionService {
+public final class SipConnectionService extends ConnectionService {
private static final String PREFIX = "[SipConnectionService] ";
private static final boolean VERBOSE = true; /* STOP SHIP if true */
- private class GetSipProfileTask extends AsyncTask<Void, Void, SipProfile> {
- private final ConnectionRequest mRequest;
- private final OutgoingCallResponse mResponse;
- private final SipProfileDb mSipProfileDb;
- private final SipSharedPreferences mSipSharedPreferences;
-
- GetSipProfileTask(
- Context context,
- ConnectionRequest request,
- OutgoingCallResponse response) {
- mRequest = request;
- mResponse = response;
- mSipProfileDb = new SipProfileDb(context);
- mSipSharedPreferences = new SipSharedPreferences(context);
- }
-
- @Override
- protected SipProfile doInBackground(Void... params) {
- String primarySipUri = mSipSharedPreferences.getPrimaryAccount();
- for (SipProfile profile : mSipProfileDb.retrieveSipProfileList()) {
- if (profile.getUriString().equals(primarySipUri)) {
- return profile;
- }
- }
- // TODO(sail): Handle non-primary profiles by showing dialog.
- return null;
- }
-
- @Override
- protected void onPostExecute(SipProfile profile) {
- onSipProfileChosen(profile, mRequest, mResponse);
- }
- }
-
@Override
protected void onCreateConnections(
- ConnectionRequest request,
- OutgoingCallResponse<Connection> callback) {
+ final ConnectionRequest request,
+ final OutgoingCallResponse<Connection> response) {
if (VERBOSE) log("onCreateConnections, request: " + request);
- new GetSipProfileTask(this, request, callback).execute();
+
+ SipProfileChooser.Callback callback = new SipProfileChooser.Callback() {
+ @Override
+ public void onSipChosen(SipProfile profile) {
+ if (VERBOSE) log("onCreateConnections, onSipChosen: " + profile);
+ SipConnection connection = createConnectionForProfile(profile, request);
+ if (connection == null) {
+ response.onCancel(request);
+ } else {
+ response.onSuccess(request, connection);
+ }
+ }
+
+ @Override
+ public void onSipNotChosen() {
+ if (VERBOSE) log("onCreateConnections, onSipNotChosen");
+ response.onFailure(request, DisconnectCause.ERROR_UNSPECIFIED, null);
+ }
+
+ @Override
+ public void onCancelCall() {
+ if (VERBOSE) log("onCreateConnections, onCancelCall");
+ response.onCancel(request);
+ }
+ };
+
+ SipProfileChooser chooser = new SipProfileChooser(this, callback);
+ chooser.start(request.getHandle(), request.getExtras());
}
@Override
protected void onCreateConferenceConnection(
String token,
Connection connection,
- Response<String, Connection> callback) {
+ Response<String, Connection> response) {
if (VERBOSE) log("onCreateConferenceConnection, connection: " + connection);
}
@Override
protected void onCreateIncomingConnection(
ConnectionRequest request,
- Response<ConnectionRequest, Connection> callback) {
+ Response<ConnectionRequest, Connection> response) {
if (VERBOSE) log("onCreateIncomingConnection, request: " + request);
+
+ Intent sipIntent = (Intent) request.getExtras().getParcelable(
+ SipUtil.EXTRA_INCOMING_CALL_INTENT);
+ if (sipIntent == null) {
+ if (VERBOSE) log("onCreateIncomingConnection, no SIP intent");
+ response.onError(request, DisconnectCause.ERROR_UNSPECIFIED, null);
+ return;
+ }
+
+ SipAudioCall sipAudioCall;
+ try {
+ sipAudioCall = SipManager.newInstance(this).takeAudioCall(sipIntent, null);
+ } catch (SipException e) {
+ log("onCreateConferenceConnection, takeAudioCall exception: " + e);
+ response.onError(request, DisconnectCause.ERROR_UNSPECIFIED, null);
+ return;
+ }
+
+ SipPhone phone = findPhoneForProfile(sipAudioCall.getLocalProfile());
+ if (phone == null) {
+ phone = createPhoneForProfile(sipAudioCall.getLocalProfile());
+ }
+ if (phone != null) {
+ com.android.internal.telephony.Connection originalConnection = phone.takeIncomingCall(
+ sipAudioCall);
+ if (VERBOSE) log("onCreateIncomingConnection, new connection: " + originalConnection);
+ if (originalConnection != null) {
+ SipConnection connection = new SipConnection(originalConnection);
+ response.onResult(getConnectionRequestForIncomingCall(request, sipAudioCall),
+ connection);
+ } else {
+ if (VERBOSE) log("onCreateIncomingConnection, takingIncomingCall failed");
+ response.onError(request, DisconnectCause.ERROR_UNSPECIFIED, null);
+ }
+ }
}
@Override
protected void onConnectionAdded(Connection connection) {
if (VERBOSE) log("onConnectionAdded, connection: " + connection);
+ if (connection instanceof SipConnection) {
+ ((SipConnection) connection).onAddedToCallService();
+ }
}
@Override
@@ -109,45 +141,71 @@
if (VERBOSE) log("onConnectionRemoved, connection: " + connection);
}
- private void onSipProfileChosen(
+ private SipConnection createConnectionForProfile(
SipProfile profile,
- ConnectionRequest request,
- OutgoingCallResponse response) {
- if (profile != null) {
- String sipUri = profile.getUriString();
- SipPhone phone = null;
- try {
- SipManager.newInstance(this).open(profile);
- phone = (SipPhone) PhoneFactory.makeSipPhone(sipUri);
- startCallWithPhone(phone, request, response);
- } catch (SipException e) {
- log("Failed to make a SIP phone: " + e);
- response.onFailure(request, DisconnectCause.ERROR_UNSPECIFIED,
- "Failed to make a SIP phone: " + e);
+ ConnectionRequest request) {
+ SipPhone phone = findPhoneForProfile(profile);
+ if (phone == null) {
+ phone = createPhoneForProfile(profile);
+ }
+ if (phone != null) {
+ return startCallWithPhone(phone, request);
+ }
+ return null;
+ }
+
+ private SipPhone findPhoneForProfile(SipProfile profile) {
+ if (VERBOSE) log("findPhoneForProfile, profile: " + profile);
+ for (Connection connection : getAllConnections()) {
+ if (connection instanceof SipConnection) {
+ SipPhone phone = ((SipConnection) connection).getPhone();
+ if (phone != null && phone.getSipUri().equals(profile.getUriString())) {
+ if (VERBOSE) log("findPhoneForProfile, found existing phone: " + phone);
+ return phone;
+ }
}
- } else {
- response.onFailure(request, DisconnectCause.ERROR_UNSPECIFIED,
- "Failed to find SIP profile");
+ }
+ if (VERBOSE) log("findPhoneForProfile, no phone found");
+ return null;
+ }
+
+ private SipPhone createPhoneForProfile(SipProfile profile) {
+ if (VERBOSE) log("createPhoneForProfile, profile: " + profile);
+ try {
+ SipManager.newInstance(this).open(profile);
+ return (SipPhone) PhoneFactory.makeSipPhone(profile.getUriString());
+ } catch (SipException e) {
+ log("createPhoneForProfile, exception: " + e);
+ return null;
}
}
- protected void startCallWithPhone(
- Phone phone,
- ConnectionRequest request,
- OutgoingCallResponse<Connection> response) {
+ private SipConnection startCallWithPhone(SipPhone phone, ConnectionRequest request) {
String number = request.getHandle().getSchemeSpecificPart();
+ if (VERBOSE) log("startCallWithPhone, number: " + number);
+
try {
- com.android.internal.telephony.Connection connection =
+ com.android.internal.telephony.Connection originalConnection =
phone.dial(number, request.getVideoState());
- SipConnection sipConnection = new SipConnection(connection);
- response.onSuccess(request, sipConnection);
+ return new SipConnection(originalConnection);
} catch (CallStateException e) {
- log("Call to Phone.dial failed with exception: " + e);
- response.onFailure(request, DisconnectCause.ERROR_UNSPECIFIED,
- "Call to Phone.dial failed with exception: " + e);
+ log("startCallWithPhone, exception: " + e);
+ return null;
}
}
+ private ConnectionRequest getConnectionRequestForIncomingCall(ConnectionRequest request,
+ SipAudioCall audioCall) {
+ SipProfile callee = audioCall.getPeerProfile();
+ String domain = callee.getSipDomain();
+ if (domain.endsWith(":5060")) {
+ domain = domain.substring(0, domain.length() - 5);
+ }
+ Uri uri = Uri.fromParts(SipUtil.SCHEME_SIP, callee.getUserName() + "@" + domain, null);
+ return new ConnectionRequest(request.getAccount(), request.getCallId(), uri,
+ request.getExtras(), 0);
+ }
+
private static void log(String msg) {
Log.d(SipUtil.LOG_TAG, PREFIX + msg);
}
diff --git a/sip/src/com/android/services/telephony/sip/SipEditor.java b/sip/src/com/android/services/telephony/sip/SipEditor.java
index a765296..2f2bdef 100644
--- a/sip/src/com/android/services/telephony/sip/SipEditor.java
+++ b/sip/src/com/android/services/telephony/sip/SipEditor.java
@@ -16,7 +16,6 @@
package com.android.services.telephony.sip;
-import com.android.internal.telephony.CallManager;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.services.telephony.sip.SipUtil;
@@ -75,7 +74,6 @@
private SipManager mSipManager;
private SipProfileDb mProfileDb;
private SipProfile mOldProfile;
- private CallManager mCallManager;
private Button mRemoveButton;
enum PreferenceKey {
@@ -146,7 +144,7 @@
public void onResume() {
super.onResume();
mHomeButtonClicked = false;
- if (mCallManager.getState() != PhoneConstants.State.IDLE) {
+ if (!SipUtil.isPhoneIdle(this)) {
mAdvancedSettings.show();
getPreferenceScreen().setEnabled(false);
if (mRemoveButton != null) mRemoveButton.setEnabled(false);
@@ -164,7 +162,6 @@
mSipManager = SipManager.newInstance(this);
mSharedPreferences = new SipSharedPreferences(this);
mProfileDb = new SipProfileDb(this);
- mCallManager = CallManager.getInstance();
setContentView(R.layout.sip_settings_ui);
addPreferencesFromResource(R.xml.sip_edit);
diff --git a/sip/src/com/android/services/telephony/sip/SipProfileChooser.java b/sip/src/com/android/services/telephony/sip/SipProfileChooser.java
new file mode 100644
index 0000000..687edfa
--- /dev/null
+++ b/sip/src/com/android/services/telephony/sip/SipProfileChooser.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.sip;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.sip.SipProfile;
+import android.net.sip.SipManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import java.util.List;
+
+/** Find a SIP profile to use for the an outgoing call. */
+final class SipProfileChooser {
+ private static final String PREFIX = "[SipProfileChooser] ";
+ private static final boolean VERBOSE = true; /* STOP SHIP if true */
+
+ interface Callback {
+ // Call will be completed by the given SIP profile.
+ void onSipChosen(SipProfile profile);
+ // Call will be tried by another connection service (GSM, CDMA, etc...).
+ void onSipNotChosen();
+ // Call will be aborted.
+ void onCancelCall();
+ }
+
+ private final Context mContext;
+ private final Callback mCallback;
+ private final SipProfileDb mSipProfileDb;
+ private List<SipProfile> mProfileList;
+ private SipProfile mPrimaryProfile;
+ private final Handler mHandler = new Handler();
+
+ SipProfileChooser(Context context, Callback callback) {
+ mContext = context;
+ mCallback = callback;
+ mSipProfileDb = new SipProfileDb(mContext);
+ }
+
+ void start(Uri handle, Bundle extras) {
+ if (VERBOSE) log("start, handle: " + handle);
+
+ String scheme = handle.getScheme();
+ if (!SipUtil.SCHEME_SIP.equals(scheme) && !SipUtil.SCHEME_TEL.equals(scheme)) {
+ if (VERBOSE) log("start, unknown scheme");
+ mCallback.onSipNotChosen();
+ return;
+ }
+
+ String phoneNumber = handle.getSchemeSpecificPart();
+ // Consider "tel:foo@exmaple.com" a SIP number.
+ boolean isSipNumber = SipUtil.SCHEME_SIP.equals(scheme) ||
+ PhoneNumberUtils.isUriNumber(phoneNumber);
+ if (!SipUtil.isVoipSupported(mContext)) {
+ if (isSipNumber) {
+ if (VERBOSE) log("start, VOIP not supported, dropping call");
+ SipProfileChooserDialogs.showNoVoip(mContext, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int choice, Bundle resultData) {
+ mCallback.onCancelCall();
+ }
+ });
+ } else {
+ if (VERBOSE) log("start, VOIP not supported");
+ mCallback.onSipNotChosen();
+ }
+ return;
+ }
+
+ // Don't use SIP for numbers modified by a gateway.
+ if (extras != null && extras.getString(SipUtil.GATEWAY_PROVIDER_PACKAGE) != null) {
+ if (VERBOSE) log("start, not using SIP for numbers modified by gateway");
+ mCallback.onSipNotChosen();
+ return;
+ }
+
+ if (!isNetworkConnected()) {
+ if (isSipNumber) {
+ if (VERBOSE) log("start, network not connected, dropping call");
+ SipProfileChooserDialogs.showNoInternetError(mContext,
+ new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int choice, Bundle resultData) {
+ mCallback.onCancelCall();
+ }
+ });
+ } else {
+ if (VERBOSE) log("start, network not connected");
+ mCallback.onSipNotChosen();
+ }
+ return;
+ }
+
+ // Only ask user to pick SIP or Cell if they're actually connected to a cell network.
+ SipSharedPreferences sipPreferences = new SipSharedPreferences(mContext);
+ String callOption = sipPreferences.getSipCallOption();
+ if (callOption.equals(Settings.System.SIP_ASK_ME_EACH_TIME) && !isSipNumber &&
+ isInCellNetwork()) {
+ if (VERBOSE) log("start, call option set to ask, asking");
+ ResultReceiver receiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int choice, Bundle resultData) {
+ if (choice == DialogInterface.BUTTON_NEGATIVE) {
+ mCallback.onCancelCall();
+ } else if (SipProfileChooserDialogs.isSelectedPhoneTypeSip(mContext,
+ choice)) {
+ buildProfileList();
+ } else {
+ mCallback.onSipNotChosen();
+ }
+ }
+ };
+ SipProfileChooserDialogs.showSelectPhoneType(mContext, receiver);
+ return;
+ }
+
+ if (callOption.equals(Settings.System.SIP_ADDRESS_ONLY) && !isSipNumber) {
+ if (VERBOSE) log("start, call option set to SIP only, not a sip number");
+ mCallback.onSipNotChosen();
+ return;
+ }
+
+ if ((mSipProfileDb.getProfilesCount() == 0) && !isSipNumber) {
+ if (VERBOSE) log("start, no SIP accounts and not sip number");
+ mCallback.onSipNotChosen();
+ return;
+ }
+
+ if (VERBOSE) log("start, building profile list");
+ buildProfileList();
+ }
+
+ private boolean isNetworkConnected() {
+ ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ NetworkInfo ni = cm.getActiveNetworkInfo();
+ if (ni != null && ni.isConnected()) {
+ return ni.getType() == ConnectivityManager.TYPE_WIFI ||
+ !SipManager.isSipWifiOnly(mContext);
+ }
+ }
+ return false;
+ }
+
+ private boolean isInCellNetwork() {
+ TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ if (telephonyManager != null) {
+ // We'd also like to check the radio's power state but there's no public API to do this.
+ int phoneType = telephonyManager.getPhoneType();
+ return phoneType != TelephonyManager.PHONE_TYPE_NONE &&
+ phoneType != TelephonyManager.PHONE_TYPE_SIP;
+ }
+ return false;
+ }
+
+ private void buildProfileList() {
+ if (VERBOSE) log("buildProfileList");
+ final SipSharedPreferences preferences = new SipSharedPreferences(mContext);
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ mProfileList = mSipProfileDb.retrieveSipProfileList();
+ if (mProfileList != null) {
+ String primarySipUri = preferences.getPrimaryAccount();
+ for (SipProfile profile : mProfileList) {
+ if (profile.getUriString().equals(primarySipUri)) {
+ mPrimaryProfile = profile;
+ break;
+ }
+ }
+ }
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onBuildProfileListDone();
+ }
+ });
+ }
+ }).start();
+ }
+
+ /**
+ * At this point we definitely want to make a SIP call, we just need to figure out which
+ * profile to use.
+ */
+ private void onBuildProfileListDone() {
+ if (VERBOSE) log("onBuildProfileListDone");
+ if (mProfileList == null || mProfileList.size() == 0) {
+ if (VERBOSE) log("onBuildProfileListDone, no profiles, showing settings dialog");
+ ResultReceiver receiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int choice, Bundle resultData) {
+ if (choice == DialogInterface.BUTTON_POSITIVE) {
+ openSipSettings();
+ }
+ mCallback.onCancelCall();
+ }
+ };
+ SipProfileChooserDialogs.showStartSipSettings(mContext, receiver);
+ } else if (mPrimaryProfile == null) {
+ if (VERBOSE) log("onBuildProfileListDone, no primary profile, showing select dialog");
+ ResultReceiver receiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int choice, Bundle resultData) {
+ if (choice >= 0 && choice < mProfileList.size()) {
+ SipProfile profile = mProfileList.get(choice);
+ if (SipProfileChooserDialogs.shouldMakeSelectedProflePrimary(mContext,
+ resultData)) {
+ SipSharedPreferences pref = new SipSharedPreferences(mContext);
+ pref.setPrimaryAccount(profile.getUriString());
+ }
+ mCallback.onSipChosen(profile);
+ } else {
+ mCallback.onCancelCall();
+ }
+ }
+ };
+ SipProfileChooserDialogs.showSelectProfile(mContext, mProfileList, receiver);
+ } else {
+ mCallback.onSipChosen(mPrimaryProfile);
+ }
+ }
+
+ private void openSipSettings() {
+ Intent newIntent = new Intent(mContext, SipSettings.class);
+ newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(newIntent);
+ }
+
+ private static void log(String msg) {
+ Log.d(SipUtil.LOG_TAG, PREFIX + msg);
+ }
+}
diff --git a/sip/src/com/android/services/telephony/sip/SipProfileChooserDialogs.java b/sip/src/com/android/services/telephony/sip/SipProfileChooserDialogs.java
new file mode 100644
index 0000000..3ba633e
--- /dev/null
+++ b/sip/src/com/android/services/telephony/sip/SipProfileChooserDialogs.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.sip;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+import java.util.List;
+
+public final class SipProfileChooserDialogs extends Activity
+ implements DialogInterface.OnClickListener,
+ DialogInterface.OnCancelListener, CompoundButton.OnCheckedChangeListener {
+ private static final String PREFIX = "[SipProfileChooserDialogs] ";
+ private static final boolean VERBOSE = true; /* STOP SHIP if true */
+
+ private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
+ private static final String EXTRA_DIALOG_ID = "dialog_id";
+ private static final String EXTRA_PROFILE_NAMES = "profile_names";
+ private static final String EXTRA_MAKE_PRIMARY = "make_primary";
+
+ private static final int DIALOG_SELECT_PHONE_TYPE = 0;
+ private static final int DIALOG_SELECT_PROFILE = 1;
+ private static final int DIALOG_START_SIP_SETTINGS = 2;
+ private static final int DIALOG_NO_INTERNET_ERROR = 3;
+ private static final int DIALOG_NO_VOIP = 4;
+
+ private TextView mUnsetPriamryHint;
+ private boolean mMakePrimary;
+
+ static void showSelectPhoneType(Context context, ResultReceiver resultReceiver) {
+ show(context, DIALOG_SELECT_PHONE_TYPE, null, resultReceiver);
+ }
+
+ static void showSelectProfile(Context context, List<SipProfile> profiles,
+ ResultReceiver resultReceiver) {
+ show(context, DIALOG_SELECT_PROFILE, profiles, resultReceiver);
+ }
+
+ static void showStartSipSettings(Context context, ResultReceiver resultReceiver) {
+ show(context, DIALOG_START_SIP_SETTINGS, null, resultReceiver);
+ }
+
+ static void showNoInternetError(Context context, ResultReceiver resultReceiver) {
+ show(context, DIALOG_NO_INTERNET_ERROR, null, resultReceiver);
+ }
+
+ static void showNoVoip(Context context, ResultReceiver resultReceiver) {
+ show(context, DIALOG_NO_VOIP, null, resultReceiver);
+ }
+
+ static boolean isSelectedPhoneTypeSip(Context context, int choice) {
+ String[] phoneTypes = context.getResources().getStringArray(R.array.phone_type_values);
+ if (choice >= 0 && choice < phoneTypes.length) {
+ return phoneTypes[choice].equals(context.getString(R.string.internet_phone));
+ }
+ return false;
+ }
+
+ static boolean shouldMakeSelectedProflePrimary(Context context, Bundle extras) {
+ return extras.getBoolean(EXTRA_MAKE_PRIMARY);
+ }
+
+ static private void show(final Context context, final int dialogId,
+ final List<SipProfile> profiles,
+ final ResultReceiver resultReceiver) {
+ if (VERBOSE) log("show, starting delayed show, dialogId: " + dialogId);
+
+ // Wait for 1 second before showing the dialog. The sometimes prevents the InCallUI from
+ // popping up on top of the dialog. See http://b/16184268
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (VERBOSE) log("show, starting activity");
+ Intent intent = new Intent(context, SipProfileChooserDialogs.class)
+ .putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
+ .putExtra(EXTRA_DIALOG_ID, dialogId)
+ .putExtra(EXTRA_PROFILE_NAMES, getProfileNames(profiles))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ context.startActivity(intent);
+ }
+ }, 1000);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int dialogId = getIntent().getIntExtra(EXTRA_DIALOG_ID, 0);
+ if (VERBOSE) log("onCreate, dialogId: " + dialogId);
+
+ // Allow this activity to be visible in front of the keyguard. (This is only necessary for
+ // obscure scenarios like the user initiating a call and then immediately pressing the Power
+ // button.)
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+
+ showDialog(dialogId);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle args) {
+ Dialog dialog;
+ switch(id) {
+ case DIALOG_SELECT_PHONE_TYPE:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.pick_outgoing_call_phone_type)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setSingleChoiceItems(R.array.phone_type_values, -1, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ case DIALOG_SELECT_PROFILE:
+ String[] profileNames = getIntent().getStringArrayExtra(EXTRA_PROFILE_NAMES);
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.pick_outgoing_sip_phone)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setSingleChoiceItems(profileNames, -1, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ addMakeDefaultCheckBox(dialog);
+ break;
+ case DIALOG_START_SIP_SETTINGS:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_sip_account_found_title)
+ .setMessage(R.string.no_sip_account_found)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(R.string.sip_menu_add, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ case DIALOG_NO_INTERNET_ERROR:
+ boolean wifiOnly = SipManager.isSipWifiOnly(this);
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(wifiOnly ? R.string.no_wifi_available_title
+ : R.string.no_internet_available_title)
+ .setMessage(wifiOnly ? R.string.no_wifi_available
+ : R.string.no_internet_available)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ case DIALOG_NO_VOIP:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_voip)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ default:
+ dialog = null;
+ }
+ if (dialog != null) {
+ //mDialogs[id] = dialog;
+ }
+ return dialog;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (VERBOSE) log("onPause");
+ }
+
+ @Override
+ public void finish() {
+ if (VERBOSE) log("finish");
+ super.finish();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (VERBOSE) log("onCheckedChanged, isChecked: " + isChecked);
+ mMakePrimary = isChecked;
+ if (isChecked) {
+ mUnsetPriamryHint.setVisibility(View.VISIBLE);
+ } else {
+ mUnsetPriamryHint.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if (VERBOSE) log("onClick, id: " + id);
+ onChoiceMade(id);
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (VERBOSE) log("onCancel");
+ onChoiceMade(DialogInterface.BUTTON_NEGATIVE);
+ }
+
+ private void onChoiceMade(int choice) {
+ ResultReceiver resultReceiver = getIntent().getParcelableExtra(EXTRA_RESULT_RECEIVER);
+ if (resultReceiver != null) {
+ Bundle extras = new Bundle();
+ extras.putBoolean(EXTRA_MAKE_PRIMARY, mMakePrimary);
+ resultReceiver.send(choice, extras);
+ }
+ finish();
+ }
+
+ private void addMakeDefaultCheckBox(Dialog dialog) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null);
+ CheckBox makePrimaryCheckBox =
+ (CheckBox) view.findViewById(com.android.internal.R.id.alwaysUse);
+ makePrimaryCheckBox.setText(R.string.remember_my_choice);
+ makePrimaryCheckBox.setOnCheckedChangeListener(this);
+ mUnsetPriamryHint = (TextView)view.findViewById(com.android.internal.R.id.clearDefaultHint);
+ mUnsetPriamryHint.setText(R.string.reset_my_choice_hint);
+ mUnsetPriamryHint.setVisibility(View.GONE);
+ ((AlertDialog)dialog).setView(view);
+ }
+
+ static private String[] getProfileNames(List<SipProfile> profiles) {
+ if (profiles == null) {
+ return null;
+ }
+
+ String[] entries = new String[profiles.size()];
+ int i = 0;
+ for (SipProfile p : profiles) {
+ entries[i++] = p.getProfileName();
+ }
+ return entries;
+ }
+
+ private static void log(String msg) {
+ Log.d(SipUtil.LOG_TAG, PREFIX + msg);
+ }
+}
diff --git a/sip/src/com/android/services/telephony/sip/SipProfileDb.java b/sip/src/com/android/services/telephony/sip/SipProfileDb.java
index bf4b6bb..7533d16 100644
--- a/sip/src/com/android/services/telephony/sip/SipProfileDb.java
+++ b/sip/src/com/android/services/telephony/sip/SipProfileDb.java
@@ -35,7 +35,7 @@
/**
* Utility class that helps perform operations on the SipProfile database.
*/
-public class SipProfileDb {
+class SipProfileDb {
private static final String PREFIX = "[SipProfileDb] ";
private static final boolean VERBOSE = true; /* STOP SHIP if true */
diff --git a/sip/src/com/android/services/telephony/sip/SipSettings.java b/sip/src/com/android/services/telephony/sip/SipSettings.java
index cf35e31..e4fca2b 100644
--- a/sip/src/com/android/services/telephony/sip/SipSettings.java
+++ b/sip/src/com/android/services/telephony/sip/SipSettings.java
@@ -16,7 +16,6 @@
package com.android.services.telephony.sip;
-import com.android.internal.telephony.CallManager;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
@@ -73,7 +72,6 @@
private PackageManager mPackageManager;
private SipManager mSipManager;
- private CallManager mCallManager;
private SipProfileDb mProfileDb;
private SipProfile mProfile; // profile that's being edited
@@ -154,7 +152,6 @@
addPreferencesFromResource(R.xml.sip_setting);
mSipListContainer = (PreferenceCategory) findPreference(PREF_SIP_LIST);
registerForReceiveCallsCheckBox();
- mCallManager = CallManager.getInstance();
updateProfilesStatus();
@@ -168,11 +165,7 @@
public void onResume() {
super.onResume();
- if (mCallManager.getState() != PhoneConstants.State.IDLE) {
- mButtonSipReceiveCalls.setEnabled(false);
- } else {
- mButtonSipReceiveCalls.setEnabled(true);
- }
+ mButtonSipReceiveCalls.setEnabled(SipUtil.isPhoneIdle(this));
}
@Override
@@ -489,8 +482,7 @@
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
- menu.findItem(MENU_ADD_ACCOUNT).setEnabled(
- mCallManager.getState() == PhoneConstants.State.IDLE);
+ menu.findItem(MENU_ADD_ACCOUNT).setEnabled(SipUtil.isPhoneIdle(this));
return super.onPrepareOptionsMenu(menu);
}
diff --git a/sip/src/com/android/services/telephony/sip/SipUtil.java b/sip/src/com/android/services/telephony/sip/SipUtil.java
index 39b5655..7a1fcf7 100644
--- a/sip/src/com/android/services/telephony/sip/SipUtil.java
+++ b/sip/src/com/android/services/telephony/sip/SipUtil.java
@@ -20,9 +20,16 @@
import android.content.Context;
import android.content.Intent;
import android.net.sip.SipManager;
+import android.telecomm.TelecommManager;
public class SipUtil {
- public static final String LOG_TAG = "SIP";
+ static final String LOG_TAG = "SIP";
+ static final String EXTRA_INCOMING_CALL_INTENT =
+ "com.android.services.telephony.sip.incoming_call_intent";
+ static final String GATEWAY_PROVIDER_PACKAGE =
+ "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE";
+ static final String SCHEME_TEL = "tel";
+ static final String SCHEME_SIP = "sip";
private static boolean sIsVoipSupported;
private static boolean sIsVoipSupportedInitialized;
@@ -41,10 +48,19 @@
return sIsVoipSupported;
}
- public static PendingIntent createIncomingCallPendingIntent(Context context) {
+ static PendingIntent createIncomingCallPendingIntent(Context context) {
Intent intent = new Intent(context, SipBroadcastReceiver.class);
intent.setAction(SipManager.ACTION_SIP_INCOMING_CALL);
return PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
+
+ static boolean isPhoneIdle(Context context) {
+ TelecommManager manager = (TelecommManager) context.getSystemService(
+ Context.TELECOMM_SERVICE);
+ if (manager != null) {
+ return !manager.isInAPhoneCall();
+ }
+ return true;
+ }
}
diff --git a/src/com/android/phone/ContactsAsyncHelper.java b/src/com/android/phone/ContactsAsyncHelper.java
deleted file mode 100644
index 10a6950..0000000
--- a/src/com/android/phone/ContactsAsyncHelper.java
+++ /dev/null
@@ -1,358 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.phone;
-
-import android.app.Notification;
-import android.content.ContentUris;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.provider.ContactsContract.Contacts;
-import android.util.Log;
-
-import com.android.internal.telephony.CallerInfo;
-import com.android.internal.telephony.Connection;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Helper class for loading contacts photo asynchronously.
- */
-public class ContactsAsyncHelper {
-
- private static final boolean DBG = false;
- private static final String LOG_TAG = "ContactsAsyncHelper";
-
- /**
- * Interface for a WorkerHandler result return.
- */
- public interface OnImageLoadCompleteListener {
- /**
- * Called when the image load is complete.
- *
- * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
- * Context, Uri, OnImageLoadCompleteListener, Object)}.
- * @param photo Drawable object obtained by the async load.
- * @param photoIcon Bitmap object obtained by the async load.
- * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
- * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
- * cookie is null.
- */
- public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
- Object cookie);
- }
-
- // constants
- private static final int EVENT_LOAD_IMAGE = 1;
-
- private final Handler mResultHandler = new Handler() {
- /** Called when loading is done. */
- @Override
- public void handleMessage(Message msg) {
- WorkerArgs args = (WorkerArgs) msg.obj;
- switch (msg.arg1) {
- case EVENT_LOAD_IMAGE:
- if (args.listener != null) {
- if (DBG) {
- Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
- " image: " + args.uri + " completed");
- }
- args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
- args.cookie);
- }
- break;
- default:
- }
- }
- };
-
- /** Handler run on a worker thread to load photo asynchronously. */
- private static Handler sThreadHandler;
-
- /** For forcing the system to call its constructor */
- @SuppressWarnings("unused")
- private static ContactsAsyncHelper sInstance;
-
- static {
- sInstance = new ContactsAsyncHelper();
- }
-
- private static final class WorkerArgs {
- public Context context;
- public Uri uri;
- public Drawable photo;
- public Bitmap photoIcon;
- public Object cookie;
- public OnImageLoadCompleteListener listener;
- }
-
- /**
- * public inner class to help out the ContactsAsyncHelper callers
- * with tracking the state of the CallerInfo Queries and image
- * loading.
- *
- * Logic contained herein is used to remove the race conditions
- * that exist as the CallerInfo queries run and mix with the image
- * loads, which then mix with the Phone state changes.
- */
- public static class ImageTracker {
-
- // Image display states
- public static final int DISPLAY_UNDEFINED = 0;
- public static final int DISPLAY_IMAGE = -1;
- public static final int DISPLAY_DEFAULT = -2;
-
- // State of the image on the imageview.
- private CallerInfo mCurrentCallerInfo;
- private int displayMode;
-
- public ImageTracker() {
- mCurrentCallerInfo = null;
- displayMode = DISPLAY_UNDEFINED;
- }
-
- /**
- * Used to see if the requested call / connection has a
- * different caller attached to it than the one we currently
- * have in the CallCard.
- */
- public boolean isDifferentImageRequest(CallerInfo ci) {
- // note, since the connections are around for the lifetime of the
- // call, and the CallerInfo-related items as well, we can
- // definitely use a simple != comparison.
- return (mCurrentCallerInfo != ci);
- }
-
- public boolean isDifferentImageRequest(Connection connection) {
- // if the connection does not exist, see if the
- // mCurrentCallerInfo is also null to match.
- if (connection == null) {
- if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null");
- return (mCurrentCallerInfo != null);
- }
- Object o = connection.getUserData();
-
- // if the call does NOT have a callerInfo attached
- // then it is ok to query.
- boolean runQuery = true;
- if (o instanceof CallerInfo) {
- runQuery = isDifferentImageRequest((CallerInfo) o);
- }
- return runQuery;
- }
-
- /**
- * Simple setter for the CallerInfo object.
- */
- public void setPhotoRequest(CallerInfo ci) {
- mCurrentCallerInfo = ci;
- }
-
- /**
- * Convenience method used to retrieve the URI
- * representing the Photo file recorded in the attached
- * CallerInfo Object.
- */
- public Uri getPhotoUri() {
- if (mCurrentCallerInfo != null) {
- return ContentUris.withAppendedId(Contacts.CONTENT_URI,
- mCurrentCallerInfo.person_id);
- }
- return null;
- }
-
- /**
- * Simple setter for the Photo state.
- */
- public void setPhotoState(int state) {
- displayMode = state;
- }
-
- /**
- * Simple getter for the Photo state.
- */
- public int getPhotoState() {
- return displayMode;
- }
- }
-
- /**
- * Thread worker class that handles the task of opening the stream and loading
- * the images.
- */
- private class WorkerHandler extends Handler {
- public WorkerHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- WorkerArgs args = (WorkerArgs) msg.obj;
-
- switch (msg.arg1) {
- case EVENT_LOAD_IMAGE:
- InputStream inputStream = null;
- try {
- try {
- inputStream = Contacts.openContactPhotoInputStream(
- args.context.getContentResolver(), args.uri, true);
- } catch (Exception e) {
- Log.e(LOG_TAG, "Error opening photo input stream", e);
- }
-
- if (inputStream != null) {
- args.photo = Drawable.createFromStream(inputStream,
- args.uri.toString());
-
- // This assumes Drawable coming from contact database is usually
- // BitmapDrawable and thus we can have (down)scaled version of it.
- args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
-
- if (DBG) {
- Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
- " token: " + msg.what + " image URI: " + args.uri);
- }
- } else {
- args.photo = null;
- args.photoIcon = null;
- if (DBG) {
- Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
- " token: " + msg.what + " image URI: " + args.uri +
- ", using default image.");
- }
- }
- } finally {
- if (inputStream != null) {
- try {
- inputStream.close();
- } catch (IOException e) {
- Log.e(LOG_TAG, "Unable to close input stream.", e);
- }
- }
- }
- break;
- default:
- }
-
- // send the reply to the enclosing class.
- Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
- reply.arg1 = msg.arg1;
- reply.obj = msg.obj;
- reply.sendToTarget();
- }
-
- /**
- * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
- * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
- * create a scaled Bitmap for the Drawable.
- */
- private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
- if (!(photo instanceof BitmapDrawable)) {
- return null;
- }
- int iconSize = context.getResources()
- .getDimensionPixelSize(R.dimen.notification_icon_size);
- Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
- int orgWidth = orgBitmap.getWidth();
- int orgHeight = orgBitmap.getHeight();
- int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
- // We want downscaled one only when the original icon is too big.
- if (longerEdge > iconSize) {
- float ratio = ((float) longerEdge) / iconSize;
- int newWidth = (int) (orgWidth / ratio);
- int newHeight = (int) (orgHeight / ratio);
- // If the longer edge is much longer than the shorter edge, the latter may
- // become 0 which will cause a crash.
- if (newWidth <= 0 || newHeight <= 0) {
- Log.w(LOG_TAG, "Photo icon's width or height become 0.");
- return null;
- }
-
- // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
- // should be smaller than the original.
- return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
- } else {
- return orgBitmap;
- }
- }
- }
-
- /**
- * Private constructor for static class
- */
- private ContactsAsyncHelper() {
- HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
- thread.start();
- sThreadHandler = new WorkerHandler(thread.getLooper());
- }
-
- /**
- * Starts an asynchronous image load. After finishing the load,
- * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
- * will be called.
- *
- * @param token Arbitrary integer which will be returned as the first argument of
- * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
- * @param context Context object used to do the time-consuming operation.
- * @param personUri Uri to be used to fetch the photo
- * @param listener Callback object which will be used when the asynchronous load is done.
- * Can be null, which means only the asynchronous load is done while there's no way to
- * obtain the loaded photos.
- * @param cookie Arbitrary object the caller wants to remember, which will become the
- * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
- * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
- */
- public static final void startObtainPhotoAsync(int token, Context context, Uri personUri,
- OnImageLoadCompleteListener listener, Object cookie) {
- // in case the source caller info is null, the URI will be null as well.
- // just update using the placeholder image in this case.
- if (personUri == null) {
- Log.wtf(LOG_TAG, "Uri is missing");
- return;
- }
-
- // Added additional Cookie field in the callee to handle arguments
- // sent to the callback function.
-
- // setup arguments
- WorkerArgs args = new WorkerArgs();
- args.cookie = cookie;
- args.context = context;
- args.uri = personUri;
- args.listener = listener;
-
- // setup message arguments
- Message msg = sThreadHandler.obtainMessage(token);
- msg.arg1 = EVENT_LOAD_IMAGE;
- msg.obj = args;
-
- if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
- ", displaying default image for now.");
-
- // notify the thread to begin working
- sThreadHandler.sendMessage(msg);
- }
-
-
-}
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index f4d82c6..15e2ccc 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -1160,6 +1160,35 @@
}
/**
+ * Make sure either system app or the caller has carrier privilege.
+ *
+ * @throws SecurityException if the caller does not have the required permission/privilege
+ */
+ private void enforceModifyPermissionOrCarrierPrivilege() {
+ try {
+ mApp.enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
+ } catch (SecurityException e) {
+ log("No modify permission, check carrier privilege next.");
+ if (hasCarrierPrivileges() != TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+ loge("No Carrier Privilege.");
+ throw new SecurityException("No modify permission or carrier privilege.");
+ }
+ }
+ }
+
+ /**
+ * Make sure the caller has carrier privilege.
+ *
+ * @throws SecurityException if the caller does not have the required permission
+ */
+ private void enforceCarrierPrivilege() {
+ if (hasCarrierPrivileges() != TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+ loge("No Carrier Privilege.");
+ throw new SecurityException("No Carrier Privilege.");
+ }
+ }
+
+ /**
* Make sure the caller has the CALL_PHONE permission.
*
* @throws SecurityException if the caller does not have the required permission
@@ -1380,7 +1409,7 @@
@Override
public int iccOpenLogicalChannel(String AID) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("iccOpenLogicalChannel: " + AID);
Integer channel = (Integer)sendRequest(CMD_OPEN_CHANNEL, AID);
@@ -1390,7 +1419,7 @@
@Override
public boolean iccCloseLogicalChannel(int channel) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("iccCloseLogicalChannel: " + channel);
if (channel < 0) {
@@ -1404,7 +1433,7 @@
@Override
public String iccTransmitApduLogicalChannel(int channel, int cla,
int command, int p1, int p2, int p3, String data) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) {
log("iccTransmitApduLogicalChannel: chnl=" + channel + " cla=" + cla +
@@ -1435,7 +1464,7 @@
@Override
public String sendEnvelopeWithStatus(String content) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
IccIoResult response = (IccIoResult)sendRequest(CMD_SEND_ENVELOPE, content);
if (response.payload == null) {
@@ -1458,7 +1487,7 @@
*/
@Override
public String nvReadItem(int itemID) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("nvReadItem: item " + itemID);
String value = (String) sendRequest(CMD_NV_READ_ITEM, itemID);
if (DBG) log("nvReadItem: item " + itemID + " is \"" + value + '"');
@@ -1475,7 +1504,7 @@
*/
@Override
public boolean nvWriteItem(int itemID, String itemValue) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("nvWriteItem: item " + itemID + " value \"" + itemValue + '"');
Boolean success = (Boolean) sendRequest(CMD_NV_WRITE_ITEM,
new Pair<Integer, String>(itemID, itemValue));
@@ -1492,7 +1521,7 @@
*/
@Override
public boolean nvWriteCdmaPrl(byte[] preferredRoamingList) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("nvWriteCdmaPrl: value: " + HexDump.toHexString(preferredRoamingList));
Boolean success = (Boolean) sendRequest(CMD_NV_WRITE_CDMA_PRL, preferredRoamingList);
if (DBG) log("nvWriteCdmaPrl: " + (success ? "ok" : "fail"));
@@ -1508,7 +1537,7 @@
*/
@Override
public boolean nvResetConfig(int resetType) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("nvResetConfig: type " + resetType);
Boolean success = (Boolean) sendRequest(CMD_NV_RESET_CONFIG, resetType);
if (DBG) log("nvResetConfig: type " + resetType + ' ' + (success ? "ok" : "fail"));
@@ -1554,7 +1583,7 @@
*/
@Override
public int getPreferredNetworkType() {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("getPreferredNetworkType");
int[] result = (int[]) sendRequest(CMD_GET_PREFERRED_NETWORK_TYPE, null);
int networkType = (result != null ? result[0] : -1);
@@ -1571,7 +1600,7 @@
*/
@Override
public boolean setPreferredNetworkType(int networkType) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("setPreferredNetworkType: type " + networkType);
Boolean success = (Boolean) sendRequest(CMD_SET_PREFERRED_NETWORK_TYPE, networkType);
if (DBG) log("setPreferredNetworkType: " + (success ? "ok" : "fail"));
@@ -1587,7 +1616,7 @@
*/
@Override
public boolean setCdmaSubscription(int subscriptionType) {
- enforceModifyPermission();
+ enforceModifyPermissionOrCarrierPrivilege();
if (DBG) log("setCdmaSubscription: type " + subscriptionType);
if (subscriptionType != mPhone.CDMA_SUBSCRIPTION_RUIM_SIM &&
subscriptionType != mPhone.CDMA_SUBSCRIPTION_NV) {
diff --git a/src/com/android/services/telephony/CdmaConnection.java b/src/com/android/services/telephony/CdmaConnection.java
index f46e64f..abd0a08 100644
--- a/src/com/android/services/telephony/CdmaConnection.java
+++ b/src/com/android/services/telephony/CdmaConnection.java
@@ -16,6 +16,8 @@
package com.android.services.telephony;
+import android.telecomm.CallCapabilities;
+
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
@@ -46,4 +48,10 @@
// no-op, we only play timed dtmf tones for cdma.
super.onStopDtmfTone();
}
+
+ @Override
+ protected int buildCallCapabilities() {
+ int capabilities = CallCapabilities.MUTE;
+ return capabilities;
+ }
}
diff --git a/src/com/android/services/telephony/GsmConnection.java b/src/com/android/services/telephony/GsmConnection.java
index 7019068..f7ab344 100644
--- a/src/com/android/services/telephony/GsmConnection.java
+++ b/src/com/android/services/telephony/GsmConnection.java
@@ -16,6 +16,8 @@
package com.android.services.telephony;
+import android.telecomm.CallCapabilities;
+
import com.android.internal.telephony.CallStateException;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
@@ -24,6 +26,7 @@
* Manages a single phone call handled by GSM.
*/
public class GsmConnection extends PstnConnection {
+ private boolean mIsConferenceCapable;
public GsmConnection(Phone phone, Connection connection) {
super(phone, connection);
@@ -43,6 +46,13 @@
super.onStopDtmfTone();
}
+ void setIsConferenceCapable(boolean isConferenceCapable) {
+ if (mIsConferenceCapable != isConferenceCapable) {
+ mIsConferenceCapable = isConferenceCapable;
+ updateCallCapabilities();
+ }
+ }
+
public void performConference() {
try {
Log.d(this, "conference - %s", this);
@@ -51,4 +61,16 @@
Log.e(this, e, "Failed to conference call.");
}
}
+
+ @Override
+ protected int buildCallCapabilities() {
+ int capabilities = CallCapabilities.MUTE | CallCapabilities.SUPPORT_HOLD;
+ if (getState() == State.ACTIVE || getState() == State.HOLDING) {
+ capabilities |= CallCapabilities.HOLD;
+ }
+ if (mIsConferenceCapable) {
+ capabilities |= CallCapabilities.MERGE_CALLS;
+ }
+ return capabilities;
+ }
}
diff --git a/src/com/android/services/telephony/PstnConnectionService.java b/src/com/android/services/telephony/PstnConnectionService.java
index 82165da..f60cd73 100644
--- a/src/com/android/services/telephony/PstnConnectionService.java
+++ b/src/com/android/services/telephony/PstnConnectionService.java
@@ -19,6 +19,7 @@
import android.net.Uri;
import android.telephony.DisconnectCause;
+import android.telecomm.CallCapabilities;
import android.telecomm.Connection;
import android.telecomm.ConnectionRequest;
import android.telecomm.Response;
@@ -200,7 +201,7 @@
// TODO: Create a more general framework for conferencing. At the moment, our goal is
// simply not to break the previous GSM-specific conferencing functionality.
if (connection instanceof GsmConnection || connection instanceof ConferenceConnection) {
- if (connection.isConferenceCapable()) {
+ if ((connection.getCallCapabilities() & CallCapabilities.MERGE_CALLS) != 0) {
callback.onResult(token,
mGsmConferenceController.createConferenceConnection(connection));
}
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 1cfabbb..17ab1c9 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -29,7 +29,7 @@
/**
* Manages a single phone call in Telephony.
*/
-class TelephonyConnection extends Connection {
+abstract class TelephonyConnection extends Connection {
private static final int EVENT_PRECISE_CALL_STATE_CHANGED = 1;
private final StateHandler mHandler = new StateHandler();
@@ -135,6 +135,15 @@
super.onSetAudioState(audioState);
}
+ protected abstract int buildCallCapabilities();
+
+ final void updateCallCapabilities() {
+ int newCallCapabilities = buildCallCapabilities();
+ if (getCallCapabilities() != newCallCapabilities) {
+ setCallCapabilities(newCallCapabilities);
+ }
+ }
+
protected void hangup(int disconnectCause) {
if (mOriginalConnection != null) {
try {
@@ -188,6 +197,7 @@
case DISCONNECTING:
break;
}
+ updateCallCapabilities();
}
}
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 717049c..0cefab6 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -111,12 +111,14 @@
final TelephonyConnection telephonyConnection =
createTelephonyConnection(request, phone, connection);
respondWithResult(request, response, telephonyConnection);
+ telephonyConnection.updateCallCapabilities();
final com.android.internal.telephony.Connection connectionFinal = connection;
PostDialListener postDialListener = new PostDialListener() {
@Override
public void onPostDialWait() {
- telephonyConnection.setPostDialWait(connectionFinal.getRemainingPostDialString());
+ telephonyConnection.setPostDialWait(
+ connectionFinal.getRemainingPostDialString());
}
};
connection.addPostDialListener(postDialListener);