Compare commits

..

4 commits

Author SHA1 Message Date
Peter Cai f092430c84 do not use Dialer UI for incoming calls if audio permission is not granted
We cannot request new permissions when Dialer UI is shown. For incoming
calls, if the Dialer UI is displayed over keyguard, then the user may
not even be able to see the permission notifications that we use for
outgoing calls.

If we just do not use the Dialer UI when the permission is not granted,
it is at most a minor annoyance for the first time. After the user has
accepted an incoming call even just once, the permission will be
granted, and the Dialer integration will start to work just fine.
2022-03-11 21:54:59 -05:00
Peter Cai 3ba7f3e207 ConnectionService: miscellaneous fixes
* Fix a few potential errors due to the use of newer APIs (minSDK is
  still only 24)
* Fix one remaining case of raw usage of generic types.
2022-03-11 21:52:49 -05:00
Peter Cai d8d9571476 ConnectionService: Dialer UI integration for incoming calls 2022-03-11 21:51:08 -05:00
Peter Cai b7b2bb0cdd ConnectionService: fix unchecked type assignments 2022-03-11 21:44:57 -05:00
2 changed files with 274 additions and 266 deletions

View file

@ -1,31 +1,5 @@
package com.cheogram.android;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.telecom.CallAudioState;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.intentfilter.androidpermissions.NotificationSettings;
import com.intentfilter.androidpermissions.PermissionManager;
import com.intentfilter.androidpermissions.models.DeniedPermissions;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
@ -33,296 +7,329 @@ import java.util.Set;
import java.util.Stack;
import java.util.Vector;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import android.os.Build;
import android.telecom.CallAudioState;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import android.Manifest;
import androidx.core.content.ContextCompat;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.ServiceConnection;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.util.Log;
import com.intentfilter.androidpermissions.PermissionManager;
import com.intentfilter.androidpermissions.NotificationSettings;
import com.intentfilter.androidpermissions.models.DeniedPermissions;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.RtpSessionActivity;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import io.michaelrocks.libphonenumber.android.NumberParseException;
public class ConnectionService extends android.telecom.ConnectionService {
public XmppConnectionService xmppConnectionService = null;
protected ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
}
public XmppConnectionService xmppConnectionService = null;
protected ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
xmppConnectionService = null;
}
};
private PermissionManager mPermissionManager;
@Override
public void onServiceDisconnected(ComponentName arg0) {
xmppConnectionService = null;
}
};
@Override
public void onCreate() {
mPermissionManager = PermissionManager.getInstance(this);
mPermissionManager.setNotificationSettings(
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
);
private PermissionManager mPermissionManager;
// From XmppActivity.connectToBackend
Intent intent = new Intent(this, XmppConnectionService.class);
intent.setAction("ui");
try {
startService(intent);
} catch (IllegalStateException e) {
Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
}
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onCreate() {
mPermissionManager = PermissionManager.getInstance(this);
mPermissionManager.setNotificationSettings(
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
);
@Override
public void onDestroy() {
unbindService(mConnection);
}
// From XmppActivity.connectToBackend
Intent intent = new Intent(this, XmppConnectionService.class);
intent.setAction("ui");
try {
startService(intent);
} catch (IllegalStateException e) {
Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
}
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
public Connection onCreateOutgoingConnection(
PhoneAccountHandle phoneAccountHandle,
ConnectionRequest request
) {
String[] gateway = phoneAccountHandle.getId().split("/", 2);
@Override
public void onDestroy() {
unbindService(mConnection);
}
String rawTel = request.getAddress().getSchemeSpecificPart();
String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
@Override
public Connection onCreateOutgoingConnection(
PhoneAccountHandle phoneAccountHandle,
ConnectionRequest request
) {
String[] gateway = phoneAccountHandle.getId().split("/", 2);
String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
try {
tel = PhoneNumberUtilWrapper.normalize(this, tel);
} catch (NumberParseException e) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
String rawTel = request.getAddress().getSchemeSpecificPart();
String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.BUSY)
);
}
String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
try {
tel = PhoneNumberUtilWrapper.normalize(this, tel);
} catch (NumberParseException e) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
CheogramConnection connection = new CheogramConnection(account, with, postDial);
if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.BUSY)
);
}
Set<String> permissions = new HashSet<>();
permissions.add(Manifest.permission.RECORD_AUDIO);
mPermissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
@Override
public void onPermissionGranted() {
connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
account,
with,
ImmutableSet.of(Media.AUDIO)
));
}
Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
CheogramConnection connection = new CheogramConnection(account, with, postDial);
@Override
public void onPermissionDenied(DeniedPermissions deniedPermissions) {
connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
}
});
Set<String> permissions = new HashSet<>();
permissions.add(Manifest.permission.RECORD_AUDIO);
mPermissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
@Override
public void onPermissionGranted() {
connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
account,
with,
ImmutableSet.of(Media.AUDIO)
));
}
connection.setInitializing();
connection.setAddress(
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
TelecomManager.PRESENTATION_ALLOWED
);
@Override
public void onPermissionDenied(DeniedPermissions deniedPermissions) {
connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
}
});
xmppConnectionService.setOnRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
);
connection.setInitializing();
connection.setAddress(
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
TelecomManager.PRESENTATION_ALLOWED
);
return connection;
}
xmppConnectionService.setOnRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
);
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
Bundle extras = request.getExtras();
String accountJid = extras.getString("account");
String withJid = extras.getString("with");
String sessionId = extras.getString("sessionId");
return connection;
}
Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
Jid with = Jid.of(withJid);
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
Bundle extras = request.getExtras();
String accountJid = extras.getString("account");
String withJid = extras.getString("with");
String sessionId = extras.getString("sessionId");
CheogramConnection connection = new CheogramConnection(account, with, null);
connection.setSessionId(sessionId);
connection.setAddress(
Uri.fromParts("tel", with.getLocal(), null),
TelecomManager.PRESENTATION_ALLOWED
);
connection.setRinging();
Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
Jid with = Jid.of(withJid);
xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
CheogramConnection connection = new CheogramConnection(account, with, null);
connection.setSessionId(sessionId);
connection.setAddress(
Uri.fromParts("tel", with.getLocal(), null),
TelecomManager.PRESENTATION_ALLOWED
);
connection.setRinging();
return connection;
}
xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
protected Account account;
protected Jid with;
protected String sessionId = null;
protected Stack<String> postDial = new Stack<>();
protected Icon gatewayIcon;
protected WeakReference<JingleRtpConnection> rtpConnection = null;
return connection;
}
CheogramConnection(Account account, Jid with, String postDialString) {
super();
this.account = account;
this.with = with;
public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
protected Account account;
protected Jid with;
protected String sessionId = null;
protected Stack<String> postDial = new Stack<>();
protected Icon gatewayIcon;
protected WeakReference<JingleRtpConnection> rtpConnection = null;
gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get(
account.getRoster().getContact(Jid.of(with.getDomain())),
AvatarService.getSystemUiAvatarSize(xmppConnectionService),
false
));
CheogramConnection(Account account, Jid with, String postDialString) {
super();
this.account = account;
this.with = with;
if (postDialString != null) {
for (int i = postDialString.length() - 1; i >= 0; i--) {
postDial.push("" + postDialString.charAt(i));
}
}
gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get(
account.getRoster().getContact(Jid.of(with.getDomain())),
AvatarService.getSystemUiAvatarSize(xmppConnectionService),
false
));
setCallerDisplayName(
account.getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
);
setAudioModeIsVoip(true);
setConnectionCapabilities(
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
);
}
if (postDialString != null) {
for (int i = postDialString.length() - 1; i >= 0; i--) {
postDial.push("" + postDialString.charAt(i));
}
}
public void setSessionId(final String sessionId) {
this.sessionId = sessionId;
}
setCallerDisplayName(
account.getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
);
setAudioModeIsVoip(true);
setConnectionCapabilities(
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
);
}
@Override
public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
if (sessionId == null || !sessionId.equals(this.sessionId)) return;
if (rtpConnection == null) {
this.with = with; // Store full JID of connection
rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
}
public void setSessionId(final String sessionId) {
this.sessionId = sessionId;
}
setStatusHints(new StatusHints(null, gatewayIcon, null));
@Override
public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
if (sessionId == null || !sessionId.equals(this.sessionId)) return;
if (rtpConnection == null) {
this.with = with; // Store full JID of connection
rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
}
if (state == RtpEndUserState.FINDING_DEVICE) {
setInitialized();
} else if (state == RtpEndUserState.RINGING) {
setDialing();
} else if (state == RtpEndUserState.INCOMING_CALL) {
setRinging();
} else if (state == RtpEndUserState.CONNECTED) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
setStatusHints(new StatusHints(null, gatewayIcon, null));
postDial();
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
setDisconnected(new DisconnectCause(DisconnectCause.BUSY));
} else if (state == RtpEndUserState.ENDED) {
setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
} else if (state == RtpEndUserState.RETRACTED) {
setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
} else if (RtpSessionActivity.END_CARD.contains(state)) {
setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
}
}
if (state == RtpEndUserState.FINDING_DEVICE) {
setInitialized();
} else if (state == RtpEndUserState.RINGING) {
setDialing();
} else if (state == RtpEndUserState.INCOMING_CALL) {
setRinging();
} else if (state == RtpEndUserState.CONNECTED) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return;
postDial();
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
setDisconnected(new DisconnectCause(DisconnectCause.BUSY));
} else if (state == RtpEndUserState.ENDED) {
setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
} else if (state == RtpEndUserState.RETRACTED) {
setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
} else if (RtpSessionActivity.END_CARD.contains(state)) {
setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
}
}
switch (selectedAudioDevice) {
case SPEAKER_PHONE:
setAudioRoute(CallAudioState.ROUTE_SPEAKER);
case WIRED_HEADSET:
setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
case EARPIECE:
setAudioRoute(CallAudioState.ROUTE_EARPIECE);
case BLUETOOTH:
setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
default:
setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
}
}
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return;
@Override
public void onAnswer() {
// For incoming calls, a connection update may not have been triggered before answering
// so we have to acquire the rtp connection object here
this.rtpConnection =
xmppConnectionService.getJingleConnectionManager()
.findJingleRtpConnection(account, with, sessionId);
switch(selectedAudioDevice) {
case SPEAKER_PHONE:
setAudioRoute(CallAudioState.ROUTE_SPEAKER);
case WIRED_HEADSET:
setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
case EARPIECE:
setAudioRoute(CallAudioState.ROUTE_EARPIECE);
case BLUETOOTH:
setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
default:
setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
}
}
@Override
public void onAnswer() {
// For incoming calls, a connection update may not have been triggered before answering
// so we have to acquire the rtp connection object here
this.rtpConnection =
xmppConnectionService.getJingleConnectionManager()
.findJingleRtpConnection(account, with, sessionId);
rtpConnection.get().acceptCall();
}
@Override
public void onDisconnect() {
if (rtpConnection == null || rtpConnection.get() == null) {
xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
} else {
rtpConnection.get().endCall();
}
destroy();
xmppConnectionService.setDiallerIntegrationActive(false);
xmppConnectionService.removeRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) this
);
}
@Override
public void onDisconnect() {
if (rtpConnection == null || rtpConnection.get() == null) {
xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
} else {
rtpConnection.get().endCall();
}
destroy();
xmppConnectionService.setDiallerIntegrationActive(false);
xmppConnectionService.removeRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) this
);
}
@Override
public void onAbort() {
onDisconnect();
}
@Override
public void onAbort() {
onDisconnect();
}
@Override
public void onPlayDtmfTone(char c) {
rtpConnection.get().applyDtmfTone("" + c);
}
@Override
public void onPlayDtmfTone(char c) {
rtpConnection.get().applyDtmfTone("" + c);
}
@Override
public void onPostDialContinue(boolean c) {
if (c) postDial();
}
@Override
public void onPostDialContinue(boolean c) {
if (c) postDial();
}
protected void sleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
protected void sleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
protected void postDial() {
while (!postDial.empty()) {
String next = postDial.pop();
if (next.equals(";")) {
Vector<String> v = new Vector<>(postDial);
Collections.reverse(v);
setPostDialWait(Joiner.on("").skipNulls().join(v));
return;
} else if (next.equals(",")) {
sleep(2000);
} else {
rtpConnection.get().applyDtmfTone(next);
sleep(100);
}
}
}
}
protected void postDial() {
while (!postDial.empty()) {
String next = postDial.pop();
if (next.equals(";")) {
Vector<String> v = new Vector<>(postDial);
Collections.reverse(v);
setPostDialWait(Joiner.on("").join(v));
return;
} else if (next.equals(",")) {
sleep(2000);
} else {
rtpConnection.get().applyDtmfTone(next);
sleep(100);
}
}
}
}
}

View file

@ -430,8 +430,9 @@ public class NotificationService {
private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set<Media> media) {
if (mXmppConnectionService.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
// We cannot always request audio permission in Dialer UI
// e.g. when Dialer is shown over keyguard
// We cannot request audio permission in Dialer UI
// when Dialer is shown over keyguard, the user cannot even necessarily
// see notifications.
return false;
}