forked from mirror/monocles_chat
Dialpad integration + contact number sync + improved tags (Cheogram)
This commit is contained in:
parent
2500a6fe11
commit
85794d85f8
25 changed files with 1269 additions and 43 deletions
|
@ -107,6 +107,7 @@ dependencies {
|
|||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
|
||||
implementation 'io.github.nishkarsh:android-permissions:2.0.54'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
@ -52,6 +53,10 @@
|
|||
android:name="android.hardware.microphone"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<package android:name="org.sufficientlysecure.keychain"/>
|
||||
</queries>
|
||||
|
||||
<queries>
|
||||
<!-- Browser -->
|
||||
<intent>
|
||||
|
@ -109,6 +114,16 @@
|
|||
tools:replace="android:label, android:allowBackup"
|
||||
tools:targetApi="r">
|
||||
|
||||
<service android:name="de.monocles.chat.ConnectionService"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
|
266
src/main/java/de/monocles/chat/ConnectionService.java
Normal file
266
src/main/java/de/monocles/chat/ConnectionService.java
Normal file
|
@ -0,0 +1,266 @@
|
|||
package de.monocles.chat;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
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.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.models.DeniedPermissions;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.RtpSessionActivity;
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName arg0) {
|
||||
xmppConnectionService = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// From XmppActivity.connectToBackend
|
||||
Intent intent = new Intent(this, XmppConnectionService.class);
|
||||
intent.setAction("ui");
|
||||
try {
|
||||
startService(intent);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(".ConnectionService", "unable to start service from " + getClass().getSimpleName());
|
||||
}
|
||||
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
unbindService(mConnection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection onCreateOutgoingConnection(
|
||||
PhoneAccountHandle phoneAccountHandle,
|
||||
ConnectionRequest request
|
||||
) {
|
||||
String[] gateway = phoneAccountHandle.getId().split("/", 2);
|
||||
|
||||
String rawTel = request.getAddress().getSchemeSpecificPart();
|
||||
String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
|
||||
|
||||
// TODO: jabber:iq:gateway
|
||||
String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
|
||||
if (tel.startsWith("1")) {
|
||||
tel = "+" + tel;
|
||||
} else if (!tel.startsWith("+")) {
|
||||
tel = "+1" + tel;
|
||||
}
|
||||
|
||||
if (xmppConnectionService.getJingleConnectionManager().isBusy()) {
|
||||
return Connection.createFailedConnection(
|
||||
new DisconnectCause(DisconnectCause.BUSY)
|
||||
);
|
||||
}
|
||||
|
||||
Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
|
||||
Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
|
||||
CheogramConnection connection = new CheogramConnection(account, with, postDial);
|
||||
|
||||
PermissionManager permissionManager = PermissionManager.getInstance(this);
|
||||
Set<String> permissions = new HashSet();
|
||||
permissions.add(Manifest.permission.RECORD_AUDIO);
|
||||
permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
|
||||
@Override
|
||||
public void onPermissionGranted() {
|
||||
connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
|
||||
account,
|
||||
with,
|
||||
ImmutableSet.of(Media.AUDIO)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionDenied(DeniedPermissions deniedPermissions) {
|
||||
connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
|
||||
}
|
||||
});
|
||||
|
||||
connection.setAddress(
|
||||
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
|
||||
TelecomManager.PRESENTATION_ALLOWED
|
||||
);
|
||||
connection.setCallerDisplayName(
|
||||
account.getDisplayName(),
|
||||
TelecomManager.PRESENTATION_ALLOWED
|
||||
);
|
||||
connection.setAudioModeIsVoip(true);
|
||||
connection.setRingbackRequested(true);
|
||||
connection.setDialing();
|
||||
connection.setConnectionCapabilities(
|
||||
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
|
||||
);
|
||||
|
||||
xmppConnectionService.setOnRtpConnectionUpdateListener(
|
||||
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
|
||||
);
|
||||
|
||||
return 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 WeakReference<JingleRtpConnection> rtpConnection = null;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSessionId(final String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
@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.CONNECTED) {
|
||||
xmppConnectionService.setDiallerIntegrationActive(true);
|
||||
setActive();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
|
||||
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 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 onPlayDtmfTone(char c) {
|
||||
rtpConnection.get().applyDtmfTone("" + c);
|
||||
}
|
||||
|
||||
@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 postDial() {
|
||||
while (!postDial.empty()) {
|
||||
String next = postDial.pop();
|
||||
if (next.equals(";")) {
|
||||
Stack v = (Stack) postDial.clone();
|
||||
Collections.reverse(v);
|
||||
setPostDialWait(String.join("", v));
|
||||
return;
|
||||
} else if (next.equals(",")) {
|
||||
sleep(2000);
|
||||
} else {
|
||||
rtpConnection.get().applyDtmfTone(next);
|
||||
sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
|||
public final class Config {
|
||||
|
||||
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5;
|
||||
|
||||
private static final int UNENCRYPTED = 1;
|
||||
private static final int OPENPGP = 2;
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package eu.siacs.conversations.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
|
||||
import io.michaelrocks.libphonenumber.android.NumberParseException;
|
||||
|
||||
public class PhoneNumberContact extends AbstractPhoneContact {
|
||||
|
||||
private final String phoneNumber;
|
||||
private final String typeLabel;
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public String getTypeLabel() {
|
||||
return typeLabel;
|
||||
}
|
||||
|
||||
private PhoneNumberContact(Context context, Cursor cursor) throws IllegalArgumentException {
|
||||
super(cursor);
|
||||
try {
|
||||
this.phoneNumber = PhoneNumberUtilWrapper.normalize(context, cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
|
||||
this.typeLabel = ContactsContract.CommonDataKinds.Phone.getTypeLabel(
|
||||
context.getResources(),
|
||||
cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)),
|
||||
cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LABEL))
|
||||
).toString();
|
||||
} catch (NumberParseException | NullPointerException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ImmutableMap<String, PhoneNumberContact> load(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
|
||||
ContactsContract.Data.DISPLAY_NAME,
|
||||
ContactsContract.Data.PHOTO_URI,
|
||||
ContactsContract.Data.LOOKUP_KEY,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE,
|
||||
ContactsContract.CommonDataKinds.Phone.LABEL,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER};
|
||||
final HashMap<String, PhoneNumberContact> contacts = new HashMap<>();
|
||||
try (final Cursor cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null)){
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
try {
|
||||
final PhoneNumberContact contact = new PhoneNumberContact(context, cursor);
|
||||
final PhoneNumberContact preexisting = contacts.get(contact.getPhoneNumber());
|
||||
if (preexisting == null || preexisting.rating() < contact.rating()) {
|
||||
contacts.put(contact.getPhoneNumber(), contact);
|
||||
}
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
return ImmutableMap.copyOf(contacts);
|
||||
}
|
||||
|
||||
public static PhoneNumberContact findByUriOrNumber(Collection<PhoneNumberContact> haystack, Uri uri, String number) {
|
||||
final PhoneNumberContact byUri = findByUri(haystack, uri);
|
||||
return byUri != null || number == null ? byUri : findByNumber(haystack, number);
|
||||
}
|
||||
|
||||
public static PhoneNumberContact findByUri(Collection<PhoneNumberContact> haystack, Uri needle) {
|
||||
for (PhoneNumberContact contact : haystack) {
|
||||
if (needle.equals(contact.getLookupUri())) {
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PhoneNumberContact findByNumber(Collection<PhoneNumberContact> haystack, String needle) {
|
||||
for (PhoneNumberContact contact : haystack) {
|
||||
if (needle.equals(contact.getPhoneNumber())) {
|
||||
return contact;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,26 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.os.Bundle;
|
||||
import android.telecom.PhoneAccount;
|
||||
import android.telecom.PhoneAccountHandle;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import eu.siacs.conversations.services.AvatarService;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.telecom.PhoneAccount;
|
||||
import android.telecom.PhoneAccountHandle;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.content.ComponentName;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -195,11 +210,17 @@ public class Contact implements ListItem, Blockable {
|
|||
for (final String group : getGroups(true)) {
|
||||
tags.add(new Tag(group, UIHelper.getColorForName(group), 0, account, isActive()));
|
||||
}
|
||||
for (final String tag : getSystemTags(true)) {
|
||||
tags.add(new Tag(tag, UIHelper.getColorForName(tag), 0, account, isActive()));
|
||||
}
|
||||
Presence.Status status = getShownStatus();
|
||||
tags.add(UIHelper.getTagForStatus(context, status, account, isActive()));
|
||||
if (isBlocked()) {
|
||||
tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b, 0, account, isActive()));
|
||||
}
|
||||
if (!showInRoster() && getSystemAccount() != null) {
|
||||
tags.add(new Tag("Android", UIHelper.getColorForName("Android"), 0, account, isActive()));
|
||||
}
|
||||
return new ArrayList<>(tags);
|
||||
}
|
||||
|
||||
|
@ -322,6 +343,15 @@ public class Contact implements ListItem, Blockable {
|
|||
return !old.equals(getDisplayName());
|
||||
}
|
||||
|
||||
public boolean setSystemTags(Collection<String> systemTags) {
|
||||
final JSONArray old = this.systemTags;
|
||||
this.systemTags = new JSONArray();
|
||||
for(String tag : systemTags) {
|
||||
this.systemTags.put(tag);
|
||||
}
|
||||
return !old.equals(this.systemTags);
|
||||
}
|
||||
|
||||
public boolean setPresenceName(String presenceName) {
|
||||
final String old = getDisplayName();
|
||||
this.presenceName = presenceName;
|
||||
|
@ -361,6 +391,7 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
public ArrayList<String> getOtrFingerprints() {
|
||||
synchronized (this.keys) {
|
||||
final ArrayList<String> fingerprints = new ArrayList<String>();
|
||||
|
@ -665,6 +696,48 @@ public class Contact implements ListItem, Blockable {
|
|||
return changed;
|
||||
}
|
||||
|
||||
protected String phoneAccountLabel() {
|
||||
return account.getJid().asBareJid().toString() +
|
||||
"/" + getJid().asBareJid().toString();
|
||||
}
|
||||
|
||||
protected PhoneAccountHandle phoneAccountHandle() {
|
||||
ComponentName componentName = new ComponentName(
|
||||
"de.monocles.chat",
|
||||
"de.monocles.chat.ConnectionService"
|
||||
);
|
||||
return new PhoneAccountHandle(componentName, phoneAccountLabel());
|
||||
}
|
||||
|
||||
// This Contact is a gateway to use for voice calls, register it with OS
|
||||
public void registerAsPhoneAccount(XmppConnectionService ctx) {
|
||||
TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
|
||||
|
||||
PhoneAccount phoneAccount = PhoneAccount.builder(
|
||||
phoneAccountHandle(),
|
||||
account.getJid().asBareJid().toString()
|
||||
).setAddress(
|
||||
Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null)
|
||||
).setIcon(
|
||||
Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false))
|
||||
).setHighlightColor(
|
||||
0x7401CF
|
||||
).setShortDescription(
|
||||
getJid().asBareJid().toString()
|
||||
).setCapabilities(
|
||||
PhoneAccount.CAPABILITY_CALL_PROVIDER
|
||||
).build();
|
||||
|
||||
telecomManager.registerPhoneAccount(phoneAccount);
|
||||
}
|
||||
|
||||
|
||||
// Unregister any associated PSTN gateway integration
|
||||
public void unregisterAsPhoneAccount(Context ctx) {
|
||||
TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
|
||||
telecomManager.unregisterPhoneAccount(phoneAccountHandle());
|
||||
}
|
||||
|
||||
public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
|
||||
if (clazz == JabberIdContact.class) {
|
||||
return Options.SYNCED_VIA_ADDRESSBOOK;
|
||||
|
|
|
@ -181,6 +181,23 @@ public class Presences {
|
|||
return null;
|
||||
}
|
||||
|
||||
public boolean anyIdentity(final String category, final String type) {
|
||||
synchronized (this.presences) {
|
||||
if (this.presences.size() == 0) {
|
||||
// https://github.com/iNPUTmice/Conversations/issues/4230
|
||||
return false;
|
||||
}
|
||||
for (Presence presence : this.presences.values()) {
|
||||
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
|
||||
if (disco != null && disco.hasIdentity(category, type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
|
||||
Map<String, String> typeMap = new HashMap<>();
|
||||
Map<String, String> nameMap = new HashMap<>();
|
||||
|
@ -205,19 +222,4 @@ public class Presences {
|
|||
return new Pair<>(typeMap, nameMap);
|
||||
}
|
||||
|
||||
public boolean anyIdentity(final String category, final String type) {
|
||||
synchronized (this.presences) {
|
||||
if (this.presences.size() == 0) {
|
||||
// https://github.com/iNPUTmice/Conversations/issues/4230
|
||||
return false;
|
||||
}
|
||||
for (Presence presence : this.presences.values()) {
|
||||
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
|
||||
if (disco != null && disco.hasIdentity(category, type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import eu.siacs.conversations.BuildConfig;
|
||||
|
||||
public abstract class AbstractQuickConversationsService {
|
||||
|
||||
|
||||
public static final String SMS_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
|
||||
|
||||
protected final XmppConnectionService service;
|
||||
|
||||
public AbstractQuickConversationsService(XmppConnectionService service) {
|
||||
|
@ -23,4 +30,6 @@ public abstract class AbstractQuickConversationsService {
|
|||
public abstract boolean isSynchronizing();
|
||||
|
||||
public abstract void considerSyncBackground(boolean force);
|
||||
}
|
||||
|
||||
public abstract void handleSmsReceived(Intent intent);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,42 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Collections;
|
||||
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.SystemClock;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.android.PhoneNumberContact;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class QuickConversationsService extends AbstractQuickConversationsService {
|
||||
|
||||
protected final AtomicInteger mRunningSyncJobs = new AtomicInteger(0);
|
||||
protected final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
|
||||
protected Attempt mLastSyncAttempt = Attempt.NULL;
|
||||
|
||||
QuickConversationsService(XmppConnectionService xmppConnectionService) {
|
||||
super(xmppConnectionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void considerSync() {
|
||||
|
||||
considerSync(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -18,11 +46,132 @@ public class QuickConversationsService extends AbstractQuickConversationsService
|
|||
|
||||
@Override
|
||||
public boolean isSynchronizing() {
|
||||
return false;
|
||||
return mRunningSyncJobs.get() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void considerSyncBackground(boolean force) {
|
||||
|
||||
mRunningSyncJobs.incrementAndGet();
|
||||
mSerialSingleThreadExecutor.execute(() -> {
|
||||
considerSync(force);
|
||||
if (mRunningSyncJobs.decrementAndGet() == 0) {
|
||||
service.updateRosterUi();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSmsReceived(Intent intent) {
|
||||
Log.d(Config.LOGTAG,"ignoring received SMS");
|
||||
}
|
||||
|
||||
protected static String getNumber(final List<String> gateways, final Contact contact) {
|
||||
final Jid jid = contact.getJid();
|
||||
if (jid.getLocal() != null && ("quicksy.im".equals(jid.getDomain()) || gateways.contains(jid.getDomain()))) {
|
||||
return jid.getLocal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void refresh(Account account, final List<String> gateways, Collection<PhoneNumberContact> phoneNumberContacts) {
|
||||
for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
|
||||
final Uri uri = contact.getSystemAccount();
|
||||
if (uri == null) {
|
||||
continue;
|
||||
}
|
||||
final String number = getNumber(gateways, contact);
|
||||
final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(phoneNumberContacts, uri, number);
|
||||
final boolean needsCacheClean;
|
||||
if (phoneNumberContact != null) {
|
||||
if (!uri.equals(phoneNumberContact.getLookupUri())) {
|
||||
Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri());
|
||||
}
|
||||
needsCacheClean = contact.setPhoneContact(phoneNumberContact);
|
||||
} else {
|
||||
needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
|
||||
Log.d(Config.LOGTAG, uri.toString() + " vanished from address book");
|
||||
}
|
||||
if (needsCacheClean) {
|
||||
service.getAvatarService().clear(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void considerSync(boolean forced) {
|
||||
final ImmutableMap<String, PhoneNumberContact> allContacts = PhoneNumberContact.load(service);
|
||||
for (final Account account : service.getAccounts()) {
|
||||
List<String> gateways = gateways(account);
|
||||
refresh(account, gateways, allContacts.values());
|
||||
if (!considerSync(account, gateways, allContacts, forced)) {
|
||||
service.syncRoster(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected List<String> gateways(final Account account) {
|
||||
List<String> gateways = new ArrayList();
|
||||
for (final Contact contact : account.getRoster().getContacts()) {
|
||||
if (contact.showInRoster() && (contact.getPresences().anyIdentity("gateway", "pstn") || contact.getPresences().anyIdentity("gateway", "sms"))) {
|
||||
gateways.add(contact.getJid().asBareJid().toString());
|
||||
}
|
||||
}
|
||||
return gateways;
|
||||
}
|
||||
|
||||
protected boolean considerSync(final Account account, final List<String> gateways, final Map<String, PhoneNumberContact> contacts, final boolean forced) {
|
||||
final int hash = Objects.hash(contacts.keySet(), gateways);
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
|
||||
if (!mLastSyncAttempt.retry(hash) && !forced) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt sync");
|
||||
return false;
|
||||
}
|
||||
mRunningSyncJobs.incrementAndGet();
|
||||
|
||||
mLastSyncAttempt = Attempt.create(hash);
|
||||
final List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
|
||||
for (Map.Entry<String, PhoneNumberContact> item : contacts.entrySet()) {
|
||||
PhoneNumberContact phoneContact = item.getValue();
|
||||
for(String gateway : gateways) {
|
||||
final Jid jid = Jid.ofLocalAndDomain(phoneContact.getPhoneNumber(), gateway);
|
||||
final Contact contact = account.getRoster().getContact(jid);
|
||||
boolean needsCacheClean = contact.setPhoneContact(phoneContact);
|
||||
needsCacheClean |= contact.setSystemTags(Collections.singleton(phoneContact.getTypeLabel()));
|
||||
if (needsCacheClean) {
|
||||
service.getAvatarService().clear(contact);
|
||||
}
|
||||
withSystemAccounts.remove(contact);
|
||||
}
|
||||
}
|
||||
for (final Contact contact : withSystemAccounts) {
|
||||
final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
|
||||
if (needsCacheClean) {
|
||||
service.getAvatarService().clear(contact);
|
||||
}
|
||||
}
|
||||
|
||||
mRunningSyncJobs.decrementAndGet();
|
||||
service.syncRoster(account);
|
||||
service.updateRosterUi();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static class Attempt {
|
||||
private final long timestamp;
|
||||
private final int hash;
|
||||
|
||||
private static final Attempt NULL = new Attempt(0, 0);
|
||||
|
||||
private Attempt(long timestamp, int hash) {
|
||||
this.timestamp = timestamp;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
public static Attempt create(int hash) {
|
||||
return new Attempt(SystemClock.elapsedRealtime(), hash);
|
||||
}
|
||||
|
||||
public boolean retry(int hash) {
|
||||
return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -293,6 +293,9 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (contact.getPresences().anyIdentity("gateway", "pstn")) {
|
||||
contact.registerAsPhoneAccount(this);
|
||||
}
|
||||
};
|
||||
private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
|
||||
private List<Account> accounts;
|
||||
|
@ -342,15 +345,22 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
};
|
||||
private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false);
|
||||
private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
|
||||
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(final int state, final String phoneNumber) {
|
||||
if (diallerIntegrationActive.get()) return;
|
||||
isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE);
|
||||
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
|
||||
mJingleConnectionManager.notifyPhoneCallStarted();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void setDiallerIntegrationActive(boolean active) {
|
||||
diallerIntegrationActive.set(active);
|
||||
}
|
||||
|
||||
//Ui callback listeners
|
||||
private final Set<OnConversationUpdate> mOnConversationUpdates = Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
|
||||
private final Set<OnShowErrorToast> mOnShowErrorToasts = Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
|
||||
|
@ -2419,6 +2429,7 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
public void syncRoster(final Account account) {
|
||||
unregisterPhoneAccounts(account);
|
||||
mRosterSyncTaskManager.execute(account, () -> databaseBackend.writeRoster(account.getRoster()));
|
||||
}
|
||||
|
||||
|
@ -4048,6 +4059,15 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
protected void unregisterPhoneAccounts(final Account account) {
|
||||
for (final Contact contact : account.getRoster().getContacts()) {
|
||||
if (!contact.showInRoster()) {
|
||||
contact.unregisterAsPhoneAccount(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void createContact(final Contact contact, final boolean autoGrant) {
|
||||
createContact(contact, autoGrant, null);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import static java.util.Arrays.asList;
|
|||
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
||||
|
||||
import android.Manifest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.PictureInPictureParams;
|
||||
|
@ -40,6 +41,7 @@ import com.google.common.collect.ImmutableSet;
|
|||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.webrtc.RendererCommon;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
@ -72,6 +74,10 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
|||
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
|
||||
|
||||
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
|
||||
public class RtpSessionActivity extends XmppActivity
|
||||
implements XmppConnectionService.OnJingleRtpConnectionUpdate,
|
||||
eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
|
||||
|
@ -86,8 +92,7 @@ public class RtpSessionActivity extends XmppActivity
|
|||
|
||||
private static final int CALL_DURATION_UPDATE_INTERVAL = 333;
|
||||
|
||||
private static final List<RtpEndUserState> END_CARD =
|
||||
Arrays.asList(
|
||||
public static final List<RtpEndUserState> END_CARD = Arrays.asList(
|
||||
RtpEndUserState.APPLICATION_ERROR,
|
||||
RtpEndUserState.SECURITY_ERROR,
|
||||
RtpEndUserState.DECLINED_OR_BUSY,
|
||||
|
@ -156,25 +161,35 @@ public class RtpSessionActivity extends XmppActivity
|
|||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getWindow()
|
||||
.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
|
||||
binding.dialpad.setClickConsumer(tag -> {
|
||||
requireRtpConnection().applyDtmfTone(tag);
|
||||
});
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
|
||||
binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
|
||||
final MenuItem help = menu.findItem(R.id.action_help);
|
||||
final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
|
||||
final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
|
||||
final MenuItem dialpad = menu.findItem(R.id.action_dialpad);
|
||||
help.setVisible(Config.HELP != null && isHelpButtonVisible());
|
||||
gotoChat.setVisible(isSwitchToConversationVisible());
|
||||
switchToVideo.setVisible(isSwitchToVideoVisible());
|
||||
dialpad.setVisible(isAudioOnlyConversation());
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
|
@ -205,6 +220,14 @@ public class RtpSessionActivity extends XmppActivity
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isAudioOnlyConversation() {
|
||||
final JingleRtpConnection connection =
|
||||
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
|
||||
|
||||
return connection != null && !connection.getMedia().contains(Media.VIDEO);
|
||||
}
|
||||
|
||||
|
||||
private boolean isSwitchToConversationVisible() {
|
||||
final JingleRtpConnection connection =
|
||||
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
|
||||
|
@ -229,6 +252,16 @@ public class RtpSessionActivity extends XmppActivity
|
|||
switchToConversation(conversation);
|
||||
}
|
||||
|
||||
private void toggleDialpadVisibility() {
|
||||
if (binding.dialpad.getVisibility() == View.VISIBLE) {
|
||||
binding.dialpad.setVisibility(View.GONE);
|
||||
}
|
||||
else {
|
||||
binding.dialpad.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_help:
|
||||
|
@ -240,6 +273,9 @@ public class RtpSessionActivity extends XmppActivity
|
|||
case R.id.action_switch_to_video:
|
||||
requestPermissionAndSwitchToVideo();
|
||||
return true;
|
||||
case R.id.action_dialpad:
|
||||
toggleDialpadVisibility();
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
@ -617,6 +653,13 @@ public class RtpSessionActivity extends XmppActivity
|
|||
.show();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
int visibility = findViewById(R.id.dialpad).getVisibility();
|
||||
outState.putInt("dialpad_visibility", visibility);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
|
|
|
@ -19,6 +19,8 @@ import static eu.siacs.conversations.ui.util.MyLinkify.removeTrailingBracket;
|
|||
import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube;
|
||||
import eu.siacs.conversations.ui.util.ShareUtil;
|
||||
import de.monocles.chat.SwipeDetector;
|
||||
import android.net.Uri;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
|
@ -75,6 +77,9 @@ import java.util.Locale;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Roster;
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.siacs.conversations.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import eu.siacs.conversations.databinding.DialpadBinding;
|
||||
import eu.siacs.conversations.R;
|
||||
|
||||
public class DialpadView extends ConstraintLayout implements View.OnClickListener {
|
||||
|
||||
protected Consumer<String> clickConsumer = null;
|
||||
|
||||
public DialpadView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public DialpadView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public DialpadView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setClickConsumer(Consumer<String> clickConsumer) {
|
||||
this.clickConsumer = clickConsumer;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
DialpadBinding binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(getContext()),
|
||||
R.layout.dialpad,
|
||||
this,
|
||||
true
|
||||
);
|
||||
binding.setDialpadView(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
clickConsumer.accept(v.getTag().toString());
|
||||
}
|
||||
|
||||
// Based on java.util.function.Consumer to avoid Android 24 dependency
|
||||
public interface Consumer<T> {
|
||||
void accept(T t);
|
||||
}
|
||||
}
|
|
@ -192,8 +192,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
return Optional.absent();
|
||||
}
|
||||
|
||||
private boolean hasMatchingRtpSession(
|
||||
final Account account, final Jid with, final Set<Media> media) {
|
||||
private String hasMatchingRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
||||
for (AbstractJingleConnection connection : this.connections.values()) {
|
||||
if (connection instanceof JingleRtpConnection) {
|
||||
final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
|
||||
|
@ -203,11 +202,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
if (rtpConnection.getId().account == account
|
||||
&& rtpConnection.getId().with.asBareJid().equals(with.asBareJid())
|
||||
&& rtpConnection.getMedia().equals(media)) {
|
||||
return true;
|
||||
return rtpConnection.getId().sessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) {
|
||||
|
@ -725,8 +724,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
return id.sessionId;
|
||||
}
|
||||
|
||||
public void proposeJingleRtpSession(
|
||||
final Account account, final Jid with, final Set<Media> media) {
|
||||
public String proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
||||
synchronized (this.rtpSessionProposals) {
|
||||
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry :
|
||||
this.rtpSessionProposals.entrySet()) {
|
||||
|
@ -739,16 +737,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
toneManager.transition(endUserState, media);
|
||||
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
|
||||
account, with, proposal.sessionId, endUserState);
|
||||
return;
|
||||
return proposal.sessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isBusy()) {
|
||||
if (hasMatchingRtpSession(account, with, media)) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"ignoring request to propose jingle session because the other party already created one for us");
|
||||
return;
|
||||
String sessionId = hasMatchingRtpSession(account, with, media);
|
||||
if (sessionId != null) {
|
||||
Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us: " + sessionId);
|
||||
return sessionId;
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"There is already a running RTP session. This should have been caught by the UI");
|
||||
|
@ -761,6 +758,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
final MessagePacket messagePacket =
|
||||
mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
|
||||
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
||||
return proposal.sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.webrtc.EglBase;
|
|||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.VideoTrack;
|
||||
import org.webrtc.DtmfSender;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -270,6 +271,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
}
|
||||
|
||||
public boolean applyDtmfTone(String tone) {
|
||||
return webRTCWrapper.applyDtmfTone(tone);
|
||||
}
|
||||
|
||||
private void receiveSessionTerminate(final JinglePacket jinglePacket) {
|
||||
respondOk(jinglePacket);
|
||||
final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
|
||||
|
|
|
@ -13,6 +13,8 @@ import eu.siacs.conversations.entities.Presence;
|
|||
import eu.siacs.conversations.entities.Presences;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
|
||||
public class RtpCapability {
|
||||
|
||||
|
@ -62,7 +64,13 @@ public class RtpCapability {
|
|||
|
||||
public static Capability check(final Contact contact, final boolean allowFallback) {
|
||||
final Presences presences = contact.getPresences();
|
||||
|
||||
if (presences.size() == 0 && allowFallback && contact.getAccount().isEnabled()) {
|
||||
Contact gateway = contact.getAccount().getRoster().getContact(Jid.of(contact.getJid().getDomain()));
|
||||
if (gateway.showInRoster() && gateway.getPresences().anyIdentity("gateway", "pstn")) {
|
||||
return Capability.AUDIO;
|
||||
}
|
||||
|
||||
return contact.getRtpCapability();
|
||||
}
|
||||
Capability result = Capability.NONE;
|
||||
|
|
|
@ -165,7 +165,7 @@ class ToneManager {
|
|||
}
|
||||
}
|
||||
|
||||
private void startTone(final int toneType, final int durationMs) {
|
||||
public void startTone(final int toneType, final int durationMs) {
|
||||
if (this.toneGenerator != null) {
|
||||
this.toneGenerator.release();;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Build;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.media.ToneGenerator;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
@ -13,6 +14,7 @@ import com.google.common.util.concurrent.Futures;
|
|||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
|
@ -34,11 +36,13 @@ import org.webrtc.SessionDescription;
|
|||
import org.webrtc.VideoTrack;
|
||||
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||
import org.webrtc.voiceengine.WebRtcAudioEffects;
|
||||
import org.webrtc.DtmfSender;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
@ -76,6 +80,25 @@ public class WebRTCWrapper {
|
|||
.add("GT-I9505") // Samsung Galaxy S4 (jfltexx)
|
||||
.build();
|
||||
|
||||
private static final int TONE_DURATION = 200;
|
||||
private static final Map<String,Integer> TONE_CODES;
|
||||
static {
|
||||
ImmutableMap.Builder<String,Integer> builder = new ImmutableMap.Builder<>();
|
||||
builder.put("0", ToneGenerator.TONE_DTMF_0);
|
||||
builder.put("1", ToneGenerator.TONE_DTMF_1);
|
||||
builder.put("2", ToneGenerator.TONE_DTMF_2);
|
||||
builder.put("3", ToneGenerator.TONE_DTMF_3);
|
||||
builder.put("4", ToneGenerator.TONE_DTMF_4);
|
||||
builder.put("5", ToneGenerator.TONE_DTMF_5);
|
||||
builder.put("6", ToneGenerator.TONE_DTMF_6);
|
||||
builder.put("7", ToneGenerator.TONE_DTMF_7);
|
||||
builder.put("8", ToneGenerator.TONE_DTMF_8);
|
||||
builder.put("9", ToneGenerator.TONE_DTMF_9);
|
||||
builder.put("*", ToneGenerator.TONE_DTMF_S);
|
||||
builder.put("#", ToneGenerator.TONE_DTMF_P);
|
||||
TONE_CODES = builder.build();
|
||||
}
|
||||
|
||||
private final EventCallback eventCallback;
|
||||
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
|
||||
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
|
||||
|
@ -700,6 +723,15 @@ public class WebRTCWrapper {
|
|||
return peerConnectionFactory;
|
||||
}
|
||||
|
||||
public boolean applyDtmfTone(String tone) {
|
||||
if (toneManager == null || peerConnection.getSenders().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
peerConnection.getSenders().get(0).dtmf().insertDtmf(tone, TONE_DURATION, 100);
|
||||
toneManager.startTone(TONE_CODES.get(tone), TONE_DURATION);
|
||||
return true;
|
||||
}
|
||||
|
||||
void addIceCandidate(IceCandidate iceCandidate) {
|
||||
requirePeerConnection().addIceCandidate(iceCandidate);
|
||||
}
|
||||
|
|
9
src/main/res/drawable/ic_dialpad_white_24dp.xml
Normal file
9
src/main/res/drawable/ic_dialpad_white_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,19c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM6,1c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM6,7c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM6,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,5c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,7c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,7c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,1c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
|
@ -90,6 +90,14 @@
|
|||
android:textAppearance="@style/TextAppearance.Conversations.Title.Monospace"
|
||||
tools:text="01:23" />
|
||||
|
||||
<eu.siacs.conversations.ui.widget.DialpadView
|
||||
layout="@layout/dialpad"
|
||||
android:id="@+id/dialpad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.makeramen.roundedimageview.RoundedImageView
|
||||
android:id="@+id/contact_photo"
|
||||
android:layout_width="@dimen/publish_avatar_size"
|
||||
|
|
385
src/main/res/layout/dialpad.xml
Normal file
385
src/main/res/layout/dialpad.xml
Normal file
|
@ -0,0 +1,385 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<data>
|
||||
<variable name="dialpadView" type="eu.siacs.conversations.ui.widget.DialpadView"/>
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/dialpad_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:paddingTop="@dimen/medium_margin"
|
||||
tools:ignore="HardcodedText">
|
||||
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_1_holder"
|
||||
android:tag="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_2_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_2_holder"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_2_holder"
|
||||
android:focusable="true" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_1"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="1" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_2_holder"
|
||||
android:tag="2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialpad_5_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_3_holder"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_1_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_2"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_2_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_2"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="ABC" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_3_holder"
|
||||
android:tag="3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_2_holder"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_2_holder"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_2_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_3"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_3_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_3"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="DEF" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_4_holder"
|
||||
android:tag="4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_5_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_5_holder"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_5_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_4"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="4" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_4_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_4"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="GHI" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_5_holder"
|
||||
android:tag="5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialpad_8_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_6_holder"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_4_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_5"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_5_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_5"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="JKL" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_6_holder"
|
||||
android:tag="6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_5_holder"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_5_holder"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_5_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_6"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="6" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_6_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_6"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="MNO" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_7_holder"
|
||||
android:tag="7"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_8_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_8_holder"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_8_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_7"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="7" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_7_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_7"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="PQRS" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_8_holder"
|
||||
android:tag="8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialpad_0_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_9_holder"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_7_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_8"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="8" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_8_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_8"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="TUV" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_9_holder"
|
||||
android:tag="9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_8_holder"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_8_holder"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_8_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_9"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_9_letters"
|
||||
style="@style/DialpadLetterStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/dialpad_9"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="@dimen/medium_margin"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="WXYZ" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_asterisk_holder"
|
||||
android:tag="*"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_0_holder"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_0_holder"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_0_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_asterisk"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="*" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_0_holder"
|
||||
android:tag="0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/dialpad_pound_holder"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_asterisk_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_0"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_plus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/dialpad_0"
|
||||
android:layout_alignBottom="@+id/dialpad_0"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_toEndOf="@+id/dialpad_0"
|
||||
android:gravity="center"
|
||||
android:paddingStart="@dimen/small_margin"
|
||||
android:paddingTop="@dimen/small_margin"
|
||||
android:text="+"
|
||||
android:textSize="@dimen/actionbar_text_size" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:onClick="@{dialpadView::onClick}"
|
||||
android:id="@+id/dialpad_pound_holder"
|
||||
android:tag="#"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/dialpad_0_holder"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/dialpad_0_holder"
|
||||
app:layout_constraintTop_toTopOf="@+id/dialpad_0_holder">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialpad_pound"
|
||||
style="@style/DialpadNumberStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginEnd="@dimen/activity_margin"
|
||||
android:text="#" />
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -8,12 +8,18 @@
|
|||
android:icon="?attr/icon_help"
|
||||
android:title="@string/help"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_dialpad"
|
||||
android:icon="@drawable/ic_dialpad_white_24dp"
|
||||
android:title="@string/action_dialpad"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_goto_chat"
|
||||
android:icon="?attr/icon_goto_chat"
|
||||
android:title="@string/switch_to_conversation"
|
||||
app:showAsAction="always" />
|
||||
<item android:id="@+id/action_switch_to_video"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/action_switch_to_video"
|
||||
android:title="@string/switch_to_video"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
|
@ -44,4 +44,11 @@
|
|||
<dimen name="local_video_preview_height">128dp</dimen>
|
||||
<dimen name="local_video_preview_width">96dp</dimen>
|
||||
<dimen name="rtp_session_duration_top_margin">24dp</dimen>
|
||||
|
||||
<dimen name="dialpad_text_size">30sp</dimen>
|
||||
<dimen name="smaller_text_size">12sp</dimen>
|
||||
<dimen name="medium_margin">8dp</dimen>
|
||||
<dimen name="activity_margin">16dp</dimen>
|
||||
<dimen name="small_margin">4dp</dimen>
|
||||
<dimen name="actionbar_text_size">20sp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_dialpad">Dialpad</string>
|
||||
<string name="action_add">New conversation</string>
|
||||
<string name="action_accounts">Manage accounts</string>
|
||||
<string name="action_end_conversation">End this conversation</string>
|
||||
|
|
|
@ -190,4 +190,13 @@
|
|||
<item name="android:visibility">gone</item>
|
||||
</style>
|
||||
|
||||
<style name="DialpadNumberStyle">
|
||||
<item name="android:includeFontPadding">false</item>
|
||||
<item name="android:textSize">@dimen/dialpad_text_size</item>
|
||||
</style>
|
||||
|
||||
<style name="DialpadLetterStyle">
|
||||
<item name="android:textSize">@dimen/smaller_text_size</item>
|
||||
<item name="android:alpha">0.8</item>
|
||||
</style>
|
||||
</resources>
|
Loading…
Add table
Reference in a new issue