Compare commits

..

4 commits

Author SHA1 Message Date
bb281ab7e0 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 22:04:58 -05:00
729b86a2fc 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 22:04:32 -05:00
0cd0678921 ConnectionService: Dialer UI integration for incoming calls 2022-03-11 22:04:29 -05:00
b7b2bb0cdd ConnectionService: fix unchecked type assignments 2022-03-11 21:44:57 -05:00
2 changed files with 276 additions and 270 deletions

View file

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

View file

@ -430,8 +430,9 @@ public class NotificationService {
private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set<Media> media) { private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set<Media> media) {
if (mXmppConnectionService.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { if (mXmppConnectionService.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
// We cannot always request audio permission in Dialer UI // We cannot request audio permission in Dialer UI
// e.g. when Dialer is shown over keyguard // when Dialer is shown over keyguard, the user cannot even necessarily
// see notifications.
return false; return false;
} }
@ -466,8 +467,8 @@ public class NotificationService {
try { try {
telecomManager.addNewIncomingCall(handle, callInfo); telecomManager.addNewIncomingCall(handle, callInfo);
} catch (SecurityException e) { } catch (SecurityException e) {
// There *could* be race conditions where the account is not registered yet // If the account is not registered or enabled, it could result in a security exception
// when an incoming call is already received // Just fall back to the built-in UI in this case.
Log.w(Config.LOGTAG, e); Log.w(Config.LOGTAG, e);
return false; return false;
} }