Compare commits

...

3 commits

Author SHA1 Message Date
Peter Cai 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
Peter Cai 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
Peter Cai 0cd0678921 ConnectionService: Dialer UI integration for incoming calls 2022-03-11 22:04:29 -05:00
3 changed files with 104 additions and 6 deletions

View file

@ -5,9 +5,12 @@ import java.util.Collections;
import java.util.HashSet;
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;
@ -114,9 +117,9 @@ public class ConnectionService extends android.telecom.ConnectionService {
PermissionManager permissionManager = PermissionManager.getInstance(this);
permissionManager.setNotificationSettings(
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
);
Set<String> permissions = new HashSet<>();
@ -150,6 +153,29 @@ public class ConnectionService extends android.telecom.ConnectionService {
return 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");
Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
Jid with = Jid.of(withJid);
CheogramConnection connection = new CheogramConnection(account, with, null);
connection.setSessionId(sessionId);
connection.setAddress(
Uri.fromParts("tel", with.getLocal(), null),
TelecomManager.PRESENTATION_ALLOWED
);
connection.setRinging();
xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
return connection;
}
public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
protected Account account;
protected Jid with;
@ -203,6 +229,8 @@ public class ConnectionService extends android.telecom.ConnectionService {
setInitialized();
} else if (state == RtpEndUserState.RINGING) {
setDialing();
} else if (state == RtpEndUserState.INCOMING_CALL) {
setRinging();
} else if (state == RtpEndUserState.CONNECTED) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
@ -221,6 +249,8 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return;
switch(selectedAudioDevice) {
case SPEAKER_PHONE:
setAudioRoute(CallAudioState.ROUTE_SPEAKER);
@ -235,6 +265,17 @@ public class ConnectionService extends android.telecom.ConnectionService {
}
}
@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) {
@ -276,9 +317,9 @@ public class ConnectionService extends android.telecom.ConnectionService {
while (!postDial.empty()) {
String next = postDial.pop();
if (next.equals(";")) {
Stack v = (Stack) postDial.clone();
Vector<String> v = new Vector<>(postDial);
Collections.reverse(v);
setPostDialWait(String.join("", v));
setPostDialWait(Joiner.on("").join(v));
return;
} else if (next.equals(",")) {
sleep(2000);

View file

@ -599,7 +599,7 @@ public class Contact implements ListItem, Blockable {
"/" + getJid().asBareJid().toString();
}
protected PhoneAccountHandle phoneAccountHandle() {
public PhoneAccountHandle phoneAccountHandle() {
ComponentName componentName = new ComponentName(
"com.cheogram.android",
"com.cheogram.android.ConnectionService"

View file

@ -1,5 +1,6 @@
package eu.siacs.conversations.services;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@ -8,6 +9,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface;
@ -16,9 +18,12 @@ import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.DisplayMetrics;
@ -423,7 +428,59 @@ public class NotificationService {
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
}
private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set<Media> media) {
if (mXmppConnectionService.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
// We cannot request audio permission in Dialer UI
// when Dialer is shown over keyguard, the user cannot even necessarily
// see notifications.
return false;
}
if (media.size() != 1 || !media.contains(Media.AUDIO)) {
// Currently our ConnectionService only handles single audio calls
Log.w(Config.LOGTAG, "only audio calls can be handled by cheogram connection service");
return false;
}
PhoneAccountHandle handle = null;
for (Contact contact : id.account.getRoster().getContacts()) {
if (!contact.getJid().getDomain().equals(id.with.getDomain()))
continue;
if (!contact.getPresences().anyIdentity("gateway", "pstn"))
continue;
handle = contact.phoneAccountHandle();
break;
}
if (handle == null) {
Log.w(Config.LOGTAG, "Could not find phone account handle for " + id.account.getJid().toString());
return false;
}
Bundle callInfo = new Bundle();
callInfo.putString("account", id.account.getJid().toString());
callInfo.putString("with", id.with.toString());
callInfo.putString("sessionId", id.sessionId);
TelecomManager telecomManager = mXmppConnectionService.getSystemService(TelecomManager.class);
try {
telecomManager.addNewIncomingCall(handle, callInfo);
} catch (SecurityException e) {
// If the account is not registered or enabled, it could result in a security exception
// Just fall back to the built-in UI in this case.
Log.w(Config.LOGTAG, e);
return false;
}
return true;
}
public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
if (tryRingingWithDialerUI(id, media)) {
return;
}
showIncomingCallNotification(id, media);
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
final int currentInterruptionFilter;