dialpad and gateway interaction

This commit is contained in:
kosyak 2023-08-15 01:21:20 +02:00
parent 8ff365613a
commit 18c41eb05e
25 changed files with 1272 additions and 143 deletions

View file

@ -79,7 +79,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:4.10.0"
implementation 'com.google.guava:guava:31.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'im.conversations.webrtc:webrtc-android:104.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

View file

@ -2,10 +2,103 @@ package eu.siacs.conversations.utils;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.xmpp.Jid;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
import io.michaelrocks.libphonenumber.android.Phonenumber;
public class PhoneNumberUtilWrapper {
private static volatile PhoneNumberUtil instance;
public static String getCountryForCode(String code) {
Locale locale = new Locale("", code);
return locale.getDisplayCountry();
}
public static String toFormattedPhoneNumber(Context context, Jid jid) {
throw new AssertionError("This method is not implemented in Conversations");
try {
return getInstance(context).format(toPhoneNumber(context, jid), PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL).replace(' ','\u202F');
} catch (Exception e) {
return jid.getEscapedLocal();
}
}
public static Phonenumber.PhoneNumber toPhoneNumber(Context context, Jid jid) throws NumberParseException {
return getInstance(context).parse(jid.getEscapedLocal(), "de");
}
public static String normalize(Context context, String input) throws IllegalArgumentException, NumberParseException {
return normalize(context, input, false);
}
public static String normalize(Context context, String input, boolean preferNetwork) throws IllegalArgumentException, NumberParseException {
final Phonenumber.PhoneNumber number = getInstance(context).parse(input, LocationProvider.getUserCountry(context, preferNetwork));
if (!getInstance(context).isValidNumber(number)) {
throw new IllegalArgumentException(String.format("%s is not a valid phone number", input));
}
return normalize(context, number);
}
public static String normalize(Context context, Phonenumber.PhoneNumber phoneNumber) {
return getInstance(context).format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
}
public static PhoneNumberUtil getInstance(final Context context) {
PhoneNumberUtil localInstance = instance;
if (localInstance == null) {
synchronized (PhoneNumberUtilWrapper.class) {
localInstance = instance;
if (localInstance == null) {
instance = localInstance = PhoneNumberUtil.createInstance(context);
}
}
}
return localInstance;
}
public static List<Country> getCountries(final Context context) {
List<Country> countries = new ArrayList<>();
for (String region : getInstance(context).getSupportedRegions()) {
countries.add(new Country(region, getInstance(context).getCountryCodeForRegion(region)));
}
return countries;
}
public static class Country implements Comparable<Country> {
private final String name;
private final String region;
private final int code;
Country(String region, int code) {
this.name = getCountryForCode(region);
this.region = region;
this.code = code;
}
public String getName() {
return name;
}
public String getRegion() {
return region;
}
public String getCode() {
return '+' + String.valueOf(code);
}
@Override
public int compareTo(Country o) {
return name.compareTo(o.name);
}
}
}

View file

@ -149,6 +149,22 @@ public class Presences {
return false;
}
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<>();

View file

@ -153,6 +153,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnBindListener;
import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnGatewayResult;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
@ -3376,6 +3377,24 @@ public class XmppConnectionService extends Service {
fetchConferenceConfiguration(conversation, null);
}
public void checkIfMuc(final Account account, final Jid jid, eu.siacs.conversations.utils.Consumer<Boolean> cb) {
if (jid.isDomainJid()) {
// Spec basically says MUC needs to have a node
// And also specifies that MUC and MUC service should have the same identity...
cb.accept(false);
return;
}
IqPacket request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
sendIqPacket(account, request, (acct, reply) -> {
ServiceDiscoveryResult result = new ServiceDiscoveryResult(reply);
cb.accept(
result.getFeatures().contains("http://jabber.org/protocol/muc") &&
result.hasIdentity("conference", null)
);
});
}
public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
@ -4741,6 +4760,24 @@ public class XmppConnectionService extends Service {
}
}
public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
IqPacket request = new IqPacket(input == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET);
request.setTo(jid);
Element query = request.query("jabber:iq:gateway");
if (input != null) {
Element prompt = query.addChild("prompt");
prompt.setContent(input);
}
sendIqPacket(account, request, (Account acct, IqPacket packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) {
callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
} else {
Element error = packet.findChild("error");
callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
}
});
}
public void fetchCaps(Account account, final Jid jid, final Presence presence) {
final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);

View file

@ -80,12 +80,14 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
getString(R.string.block_jabber_id),
getString(R.string.block),
null,
null,
account.getJid().asBareJid().toEscapedString(),
true,
false
false,
EnterJidDialog.SanityCheck.NO
);
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
Blockable blockable = new RawBlockable(account, contactJid);
if (xmppConnectionService.sendBlockRequest(blockable, false)) {
Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();

View file

@ -314,13 +314,15 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
mActivatedAccounts,
getString(R.string.enter_contact),
getString(R.string.select),
null,
jid == null ? null : jid.asBareJid().toString(),
getIntent().getStringExtra(EXTRA_ACCOUNT),
true,
false
false,
EnterJidDialog.SanityCheck.NO
);
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
final Intent request = getIntent();
final Intent data = new Intent();
data.putExtra("contact", contactJid.toString());

View file

@ -2,73 +2,106 @@ package eu.siacs.conversations.ui;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.EnterJidDialogBinding;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.ui.util.DelayedHintHelper;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnGatewayResult;
public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher {
private static final List<String> SUSPICIOUS_DOMAINS =
Arrays.asList("conference", "muc", "room", "rooms", "chat");
Arrays.asList("conference", "muc", "room", "rooms");
private OnEnterJidDialogPositiveListener mListener = null;
private static final String TITLE_KEY = "title";
private static final String POSITIVE_BUTTON_KEY = "positive_button";
private static final String SECONDARY_BUTTON_KEY = "secondary_button";
private static final String PREFILLED_JID_KEY = "prefilled_jid";
private static final String ACCOUNT_KEY = "account";
private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid";
private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
private static final String SANITY_CHECK_JID = "sanity_check_jid";
private static final String SHOW_BOOKMARK_CHECKBOX = "show_bookmark_checkbox";
private KnownHostsAdapter knownHostsAdapter;
private Collection<String> whitelistedDomains = Collections.emptyList();
private EnterJidDialogBinding binding;
private AlertDialog dialog;
private boolean sanityCheckJid = false;
private SanityCheck sanityCheckJid = SanityCheck.NO;
private boolean issuedWarning = false;
private GatewayListAdapter gatewayListAdapter = new GatewayListAdapter();
public static enum SanityCheck {
NO,
YES,
ALLOW_MUC
}
public static EnterJidDialog newInstance(
final List<String> activatedAccounts,
final String title,
final String positiveButton,
final String secondaryButton,
final String prefilledJid,
final String account,
boolean allowEditJid,
final boolean sanity_check_jid) {
boolean showBookmarkCheckbox,
final SanityCheck sanity_check_jid) {
EnterJidDialog dialog = new EnterJidDialog();
Bundle bundle = new Bundle();
bundle.putString(TITLE_KEY, title);
bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
bundle.putString(SECONDARY_BUTTON_KEY, secondaryButton);
bundle.putString(PREFILLED_JID_KEY, prefilledJid);
bundle.putString(ACCOUNT_KEY, account);
bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) activatedAccounts);
bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid);
bundle.putInt(SANITY_CHECK_JID, sanity_check_jid.ordinal());
bundle.putBoolean(SHOW_BOOKMARK_CHECKBOX, showBookmarkCheckbox);
dialog.setArguments(bundle);
return dialog;
}
@ -110,7 +143,11 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
binding.jid.setCursorVisible(false);
}
}
sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false);
sanityCheckJid = SanityCheck.values()[getArguments().getInt(SANITY_CHECK_JID, SanityCheck.NO.ordinal())];
if (!getArguments().getBoolean(SHOW_BOOKMARK_CHECKBOX, false)) {
binding.bookmark.setVisibility(View.GONE);
}
DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
@ -129,64 +166,112 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
binding.account.setAdapter(adapter);
}
builder.setView(binding.getRoot());
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
this.dialog = builder.create();
binding.gatewayList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
binding.gatewayList.setAdapter(gatewayListAdapter);
gatewayListAdapter.setOnEmpty(() -> binding.gatewayList.setVisibility(View.GONE));
gatewayListAdapter.setOnNonEmpty(() -> binding.gatewayList.setVisibility(View.VISIBLE));
View.OnClickListener dialogOnClick =
v -> {
handleEnter(binding, account);
};
binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView accountSpinner, View view, int position, long id) {
XmppActivity context = (XmppActivity) getActivity();
if (context == null || context.xmppConnectionService == null || accountJid() == null) return;
gatewayListAdapter.clear();
final Account account = context.xmppConnectionService.findAccountByJid(accountJid());
if (account == null) return;
for (final Contact contact : account.getRoster().getContacts()) {
if (contact.showInRoster() && (contact.getPresences().anyIdentity("gateway", null) || contact.getPresences().anySupport("jabber:iq:gateway"))) {
context.xmppConnectionService.fetchFromGateway(account, contact.getJid(), null, (final String prompt, String errorMessage) -> {
if (prompt == null && !contact.getPresences().anyIdentity("gateway", null)) return;
context.runOnUiThread(() -> {
gatewayListAdapter.add(contact, prompt);
});
});
}
}
}
@Override
public void onNothingSelected(AdapterView accountSpinner) {
gatewayListAdapter.clear();
}
});
builder.setView(binding.getRoot());
builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null);
if (getArguments().getString(SECONDARY_BUTTON_KEY) == null) {
builder.setNegativeButton(R.string.cancel, null);
} else {
builder.setNegativeButton(getArguments().getString(SECONDARY_BUTTON_KEY), null);
builder.setNeutralButton(R.string.cancel, null);
}
this.dialog = builder.create();
binding.jid.setOnEditorActionListener(
(v, actionId, event) -> {
handleEnter(binding, account);
handleEnter(binding, account, false);
return true;
});
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((v) -> handleEnter(binding, account, false));
if (getArguments().getString(SECONDARY_BUTTON_KEY) != null) {
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener((v) -> handleEnter(binding, account, true));
}
return dialog;
}
private void handleEnter(EnterJidDialogBinding binding, String account) {
final Jid accountJid;
protected Jid accountJid() {
try {
if (Config.DOMAIN_LOCK != null) {
return Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null);
} else {
return Jid.ofEscaped((String) binding.account.getSelectedItem());
}
} catch (final IllegalArgumentException e) {
return null;
}
}
private void handleEnter(EnterJidDialogBinding binding, String account, boolean secondary) {
if (!binding.account.isEnabled() && account == null) {
return;
}
try {
if (Config.DOMAIN_LOCK != null) {
accountJid =
Jid.ofEscaped(
(String) binding.account.getSelectedItem(),
Config.DOMAIN_LOCK,
null);
} else {
accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem());
}
} catch (final IllegalArgumentException e) {
final Jid accountJid = accountJid();
final OnGatewayResult finish = (final String jidString, final String errorMessage) -> {
Activity context = getActivity();
if (context == null) return; // Race condition, we got the reply after the UI was closed
context.runOnUiThread(() -> {
if (errorMessage != null) {
binding.jidLayout.setError(errorMessage);
return;
}
final Jid contactJid;
if (jidString == null) {
binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
return;
}
Jid contactJid = null;
try {
contactJid = Jid.ofEscaped(binding.jid.getText().toString().trim());
contactJid = Jid.ofEscaped(jidString);
} catch (final IllegalArgumentException e) {
binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
return;
}
if (!issuedWarning && sanityCheckJid) {
if (!issuedWarning && sanityCheckJid != SanityCheck.NO) {
if (contactJid.isDomainJid()) {
binding.jidLayout.setError(
getActivity().getString(R.string.this_looks_like_a_domain));
binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
}
if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
binding.jidLayout.setError(
getActivity().getString(R.string.this_looks_like_channel));
if (sanityCheckJid != SanityCheck.ALLOW_MUC && suspiciousSubDomain(contactJid.getDomain().toEscapedString())) {
binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
@ -195,7 +280,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
if (mListener != null) {
try {
if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
if (mListener.onEnterJidDialogPositive(accountJid, contactJid, secondary, binding.bookmark.isChecked())) {
dialog.dismiss();
}
} catch (JidError error) {
@ -204,6 +289,31 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
issuedWarning = false;
}
}
});
};
Pair<String,Pair<Jid,Presence>> p = gatewayListAdapter.getSelected();
final String type = gatewayListAdapter.getSelectedType();
// Resolve based on local settings before submission
if (type != null && (type.equals("pstn") || type.equals("sms"))) {
try {
binding.jid.setText(PhoneNumberUtilWrapper.normalize(getActivity(), binding.jid.getText().toString(), true));
} catch (NumberParseException | IllegalArgumentException | NullPointerException e) { }
}
if (p == null) {
finish.onGatewayResult(binding.jid.getText().toString().trim(), null);
} else if (p.first != null) { // Gateway already responsed to jabber:iq:gateway once
final Account acct = ((XmppActivity) getActivity()).xmppConnectionService.findAccountByJid(accountJid);
((XmppActivity) getActivity()).xmppConnectionService.fetchFromGateway(acct, p.second.first, binding.jid.getText().toString().trim(), finish);
} else if (p.second.first.isDomainJid() && p.second.second.getServiceDiscoveryResult().getFeatures().contains("jid\\20escaping")) {
finish.onGatewayResult(Jid.ofLocalAndDomain(binding.jid.getText().toString().trim(), p.second.first.getDomain().toString()).toString(), null);
} else if (p.second.first.isDomainJid()) {
finish.onGatewayResult(Jid.ofLocalAndDomain(binding.jid.getText().toString().trim().replace("@", "%"), p.second.first.getDomain().toString()).toString(), null);
} else {
finish.onGatewayResult(null, null);
}
}
public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) {
@ -244,7 +354,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
}
public interface OnEnterJidDialogPositiveListener {
boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError;
boolean onEnterJidDialogPositive(Jid account, Jid contact, boolean secondary, boolean save) throws EnterJidDialog.JidError;
}
public static class JidError extends Exception {
@ -276,4 +386,210 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
final String[] parts = domain.split("\\.");
return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]);
}
protected class GatewayListAdapter extends RecyclerView.Adapter<GatewayListAdapter.ViewHolder> {
protected class ViewHolder extends RecyclerView.ViewHolder {
protected ToggleButton button;
protected int index;
public ViewHolder(View view, int i) {
super(view);
this.button = (ToggleButton) view.findViewById(R.id.button);
setIndex(i);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
button.setChecked(true); // Force visual not to flap to unchecked
setSelected(index);
}
});
}
public void setIndex(int i) {
this.index = i;
button.setChecked(selected == i);
}
public void useButton(int res) {
button.setText(res);
button.setTextOff(button.getText());
button.setTextOn(button.getText());
button.setChecked(selected == this.index);
binding.gatewayList.setVisibility(View.VISIBLE);
button.setVisibility(View.VISIBLE);
}
public void useButton(String txt) {
button.setTextOff(txt);
button.setTextOn(txt);
button.setChecked(selected == this.index);
binding.gatewayList.setVisibility(View.VISIBLE);
button.setVisibility(View.VISIBLE);
}
}
protected List<Pair<Contact,String>> gateways = new ArrayList();
protected int selected = 0;
protected Runnable onEmpty = () -> {};
protected Runnable onNonEmpty = () -> {};
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.enter_jid_dialog_gateway_list_item, null);
return new ViewHolder(view, i);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
viewHolder.setIndex(i);
if(i == 0) {
viewHolder.useButton(R.string.account_settings_jabber_id);
} else {
viewHolder.useButton(getLabel(i));
}
}
@Override
public int getItemCount() {
return this.gateways.size() + 1;
}
public void setSelected(int i) {
int old = this.selected;
this.selected = i;
if(i == 0) {
binding.jid.setThreshold(1);
binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
binding.jidLayout.setHint(R.string.account_settings_jabber_id);
if(binding.jid.hasFocus()) {
binding.jid.setHint(R.string.account_settings_example_jabber_id);
} else {
DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
}
} else {
binding.jid.setThreshold(999999); // do not autocomplete
binding.jid.setHint(null);
binding.jid.setOnFocusChangeListener((v, hasFocus) -> {});
binding.jidLayout.setHint(this.gateways.get(i-1).second);
String type = getType(i);
if (type == null) type = "";
if (type.equals("pstn") || type.equals("sms")) {
binding.jid.setInputType(InputType.TYPE_CLASS_PHONE);
} else if (type.equals("email") || type.equals("sip")) {
binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
if(binding.jid.hasFocus()) {
binding.jid.setHint(R.string.account_settings_example_jabber_id);
} else {
DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
}
} else {
binding.jid.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
}
notifyItemChanged(old);
notifyItemChanged(i);
}
public String getLabel(Contact gateway) {
String type = getType(gateway);
if (type != null) return type;
return gateway.getDisplayName();
}
public String getLabel(int i) {
if (i == 0) return null;
return getLabel(this.gateways.get(i-1).first);
}
public String getType(int i) {
if (i == 0) return null;
return getType(this.gateways.get(i-1).first);
}
public String getType(Contact gateway) {
List<String> types = getTypes(gateway);
return types.isEmpty() ? null : types.get(0);
}
public List<String> getTypes(Contact gateway) {
List<String> types = new ArrayList<>();
for(Presence p : gateway.getPresences().getPresences()) {
if(p.getServiceDiscoveryResult() != null) {
for (ServiceDiscoveryResult.Identity id : p.getServiceDiscoveryResult().getIdentities()) {
if ("gateway".equals(id.getCategory())) types.add(id.getType());
}
}
}
return types;
}
public String getSelectedType() {
return getType(selected);
}
public Pair<String, Pair<Jid,Presence>> getSelected() {
if(this.selected == 0) {
return null; // No gateway, just use direct JID entry
}
Pair<Contact,String> gateway = this.gateways.get(this.selected - 1);
Pair<Jid,Presence> presence = null;
for (Map.Entry<String,Presence> e : gateway.first.getPresences().getPresencesMap().entrySet()) {
Presence p = e.getValue();
if (p.getServiceDiscoveryResult() != null) {
if (p.getServiceDiscoveryResult().getFeatures().contains("jabber:iq:gateway")) {
if (e.getKey().equals("")) {
presence = new Pair<>(gateway.first.getJid(), p);
} else {
presence = new Pair<>(gateway.first.getJid().withResource(e.getKey()), p);
}
break;
}
if (p.getServiceDiscoveryResult().hasIdentity("gateway", null)) {
if (e.getKey().equals("")) {
presence = new Pair<>(gateway.first.getJid(), p);
} else {
presence = new Pair<>(gateway.first.getJid().withResource(e.getKey()), p);
}
}
}
}
return presence == null ? null : new Pair(gateway.second, presence);
}
public void setOnEmpty(Runnable r) {
onEmpty = r;
}
public void setOnNonEmpty(Runnable r) {
onNonEmpty = r;
}
public void clear() {
gateways.clear();
onEmpty.run();
notifyDataSetChanged();
setSelected(0);
}
public void add(Contact gateway, String prompt) {
if (getItemCount() < 2) onNonEmpty.run();
this.gateways.add(new Pair<>(gateway, prompt));
Collections.sort(this.gateways, (x, y) -> getLabel(x.first).compareTo(getLabel(y.first)));
notifyDataSetChanged();
}
}
}

View file

@ -10,7 +10,6 @@ import android.app.PictureInPictureParams;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
@ -40,6 +39,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;
@ -58,6 +58,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.widget.DialpadView;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
@ -86,7 +87,7 @@ public class RtpSessionActivity extends XmppActivity
private static final int CALL_DURATION_UPDATE_INTERVAL = 333;
private static final List<RtpEndUserState> END_CARD =
public static final List<RtpEndUserState> END_CARD =
Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.SECURITY_ERROR,
@ -164,6 +165,17 @@ public class RtpSessionActivity extends XmppActivity
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
setSupportActionBar(binding.toolbar);
binding.dialpad.setClickConsumer(tag -> {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
if (connection != null) connection.applyDtmfTone(tag);
});
if (savedInstanceState != null) {
boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
}
}
@Override
@ -171,10 +183,12 @@ public class RtpSessionActivity extends XmppActivity
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 dialpad = menu.findItem(R.id.action_dialpad);
final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
help.setVisible(Config.HELP != null && isHelpButtonVisible());
gotoChat.setVisible(isSwitchToConversationVisible());
switchToVideo.setVisible(isSwitchToVideoVisible());
dialpad.setVisible(isAudioOnlyConversation());
return super.onCreateOptionsMenu(menu);
}
@ -212,6 +226,13 @@ public class RtpSessionActivity extends XmppActivity
&& STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
}
private boolean isAudioOnlyConversation() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
return connection != null && !connection.getMedia().contains(Media.VIDEO);
}
private boolean isSwitchToVideoVisible() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
@ -229,6 +250,15 @@ 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:
@ -237,6 +267,9 @@ public class RtpSessionActivity extends XmppActivity
case R.id.action_goto_chat:
switchToConversation();
return true;
case R.id.action_dialpad:
toggleDialpadVisibility();
return true;
case R.id.action_switch_to_video:
requestPermissionAndSwitchToVideo();
return true;
@ -407,7 +440,7 @@ public class RtpSessionActivity extends XmppActivity
private void putScreenInCallMode(final Set<Media> media) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (Media.audioOnly(media)) {
if (!media.contains(Media.VIDEO)) {
final JingleRtpConnection rtpConnection =
rtpConnectionReference != null ? rtpConnectionReference.get() : null;
final AppRTCAudioManager audioManager =
@ -418,15 +451,6 @@ public class RtpSessionActivity extends XmppActivity
acquireProximityWakeLock();
}
}
lockOrientation(media);
}
private void lockOrientation(final Set<Media> media) {
if (Media.audioOnly(media)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
@SuppressLint("WakelockTimeout")
@ -552,7 +576,7 @@ public class RtpSessionActivity extends XmppActivity
}
}
private void setWidth(final RtpEndUserState state) {
private void setWith(final RtpEndUserState state) {
setWith(getWith(), state);
}
@ -759,8 +783,9 @@ public class RtpSessionActivity extends XmppActivity
.getJingleConnectionManager()
.getTerminalSessionState(with, sessionId);
if (terminatedRtpSession == null) {
throw new IllegalStateException(
"failed to initialize activity with running rtp session. session not found");
Log.e(Config.LOGTAG, "failed to initialize activity with running rtp session. session not found");
finish();
return true;
}
initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
return true;
@ -781,7 +806,7 @@ public class RtpSessionActivity extends XmppActivity
requireRtpConnection().getState())) {
putScreenInCallMode();
}
setWidth(currentState);
setWith(currentState);
updateVideoViews(currentState);
updateStateDisplay(currentState, media, contentAddition);
updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
@ -1389,7 +1414,6 @@ public class RtpSessionActivity extends XmppActivity
final AbstractJingleConnection.Id id = requireRtpConnection().getId();
final boolean verified = requireRtpConnection().isVerified();
final Set<Media> media = getMedia();
lockOrientation(media);
final ContentAddition contentAddition = getPendingContentAddition();
final Contact contact = getWith();
if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
@ -1453,6 +1477,12 @@ public class RtpSessionActivity extends XmppActivity
}
}
@Override
protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE);
}
private void updateRtpSessionProposalState(
final Account account, final Jid with, final RtpEndUserState state) {
final Intent currentIntent = getIntent();

View file

@ -517,15 +517,17 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
ft.addToBackStack(null);
EnterJidDialog dialog = EnterJidDialog.newInstance(
mActivatedAccounts,
getString(R.string.add_contact),
getString(R.string.add),
getString(R.string.start_conversation),
getString(R.string.message),
"Call",
prefilledJid,
invite == null ? null : invite.account,
invite == null || !invite.hasFingerprints(),
true
true,
EnterJidDialog.SanityCheck.ALLOW_MUC
);
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, call, save) -> {
if (!xmppConnectionServiceBound) {
return false;
}
@ -534,25 +536,57 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (account == null) {
return true;
}
final Contact contact = account.getRoster().getContact(contactJid);
if (invite != null && invite.getName() != null) {
contact.setServerName(invite.getName());
}
if (contact.isSelf()) {
switchToConversation(contact);
if (contact.isSelf() || contact.showInRoster()) {
switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
return true;
} else if (contact.showInRoster()) {
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
}
xmppConnectionService.checkIfMuc(account, contactJid, (isMuc) -> {
if (isMuc) {
if (save) {
Bookmark bookmark = account.getBookmark(contactJid);
if (bookmark != null) {
openConversationsForBookmark(bookmark);
} else {
bookmark = new Bookmark(account, contactJid.asBareJid());
bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
final String nick = contactJid.getResource();
if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
bookmark.setNick(nick);
}
xmppConnectionService.createBookmark(account, bookmark);
final Conversation conversation = xmppConnectionService
.findOrCreateConversation(account, contactJid, true, true, true);
bookmark.setConversation(conversation);
switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
}
} else {
final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, true, true, true);
switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
}
} else {
if (save) {
final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
xmppConnectionService.createContact(contact, true, preAuth);
if (invite != null && invite.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
}
switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody());
return true;
}
switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
}
try {
dialog.dismiss();
} catch (final IllegalStateException e) { }
});
return false;
});
dialog.show(ft, FRAGMENT_TAG_DIALOG);
}

View file

@ -551,6 +551,11 @@ public abstract class XmppActivity extends ActionBarActivity {
switchToConversation(conversation, text, false, null, false, true);
}
protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
switchToConversation(conversation, body, false, null, false, true, postInit);
}
public void highlightInMuc(Conversation conversation, String nick) {
switchToConversation(conversation, null, false, nick, false, false);
}
@ -559,7 +564,13 @@ public abstract class XmppActivity extends ActionBarActivity {
switchToConversation(conversation, null, false, nick, true, false);
}
private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null);
}
public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) {
if (conversation == null) return;
Intent intent = new Intent(this, ConversationsActivity.class);
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
@ -576,6 +587,7 @@ public abstract class XmppActivity extends ActionBarActivity {
if (doNotAppend) {
intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
}
intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit);
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();

View file

@ -0,0 +1,69 @@
/*
* 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.R;
import eu.siacs.conversations.databinding.DialpadBinding;
import eu.siacs.conversations.utils.Consumer;
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());
}
}

View file

@ -21,19 +21,25 @@ public class LocationProvider {
public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0);
public static String getUserCountry(final Context context) {
return getUserCountry(context, false);
}
public static String getUserCountry(final Context context, boolean preferNetwork) {
try {
final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class);
if (tm == null) {
return getUserCountryFallback();
}
final String simCountry = tm.getSimCountryIso();
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
return simCountry.toUpperCase(Locale.US);
} else if (tm.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { // device is not 3G (would be unreliable)
String networkCountry = tm.getNetworkCountryIso();
if (networkCountry != null && networkCountry.length() == 2) { // network country code is available
final String simCountry = tm.getSimOperator().equals("20801") ? "us" : tm.getSimCountryIso();
final String networkCountry = tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA ? null : tm.getNetworkCountryIso(); // if device is not 3G would be unreliable
if (preferNetwork && networkCountry != null && networkCountry.length() == 2) {
return networkCountry.toUpperCase(Locale.US);
}
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
return simCountry.toUpperCase(Locale.US);
} else if (networkCountry != null && networkCountry.length() == 2) { // network country code is available
return networkCountry.toUpperCase(Locale.US);
}
return getUserCountryFallback();
} catch (final Exception e) {

View file

@ -0,0 +1,7 @@
package eu.siacs.conversations.xmpp;
public interface OnGatewayResult {
// if prompt is null, there was an error
// errorText may or may not be set
public void onGatewayResult(String prompt, String errorText);
}

View file

@ -270,6 +270,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();

View file

@ -148,7 +148,7 @@ class ToneManager {
private void scheduleWaitingTone() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
startTone(ToneGenerator.TONE_CDMA_NETWORK_USA_RINGBACK, 750);
}, 0, 3, TimeUnit.SECONDS);
}
@ -161,14 +161,16 @@ class ToneManager {
currentTone.cancel(true);
}
if (toneGenerator != null) {
// catch race condition with already-released generator
try {
toneGenerator.stopTone();
} catch (final RuntimeException e) { }
}
}
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();;
this.toneGenerator.release();
}
final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class);
final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
import android.media.ToneGenerator;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@ -8,6 +9,7 @@ import android.util.Log;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -20,6 +22,7 @@ import org.webrtc.CandidatePairChangeEvent;
import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.DtmfSender;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
@ -38,6 +41,7 @@ import org.webrtc.voiceengine.WebRtcAudioEffects;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -57,6 +61,25 @@ public class WebRTCWrapper {
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private static final int TONE_DURATION = 500;
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 static final Set<String> HARDWARE_AEC_BLACKLIST =
new ImmutableSet.Builder<String>()
.add("Pixel")
@ -510,8 +533,14 @@ public class WebRTCWrapper {
}
boolean isMicrophoneEnabled() {
final Optional<AudioTrack> audioTrack =
TrackWrapper.get(peerConnection, this.localAudioTrack);
Optional<AudioTrack> audioTrack = null;
try {
audioTrack = TrackWrapper.get(peerConnection, this.localAudioTrack);
} catch (final IllegalStateException e) {
Log.d(Config.LOGTAG, "unable to check microphone", e);
// ignoring race condition in case sender has been disposed
return false;
}
if (audioTrack.isPresent()) {
try {
return audioTrack.get().enabled();
@ -526,8 +555,14 @@ public class WebRTCWrapper {
}
boolean setMicrophoneEnabled(final boolean enabled) {
final Optional<AudioTrack> audioTrack =
TrackWrapper.get(peerConnection, this.localAudioTrack);
Optional<AudioTrack> audioTrack = null;
try {
audioTrack = TrackWrapper.get(peerConnection, this.localAudioTrack);
} catch (final IllegalStateException e) {
Log.d(Config.LOGTAG, "unable to toggle microphone", e);
// ignoring race condition in case sender has been disposed
return false;
}
if (audioTrack.isPresent()) {
try {
audioTrack.get().setEnabled(enabled);
@ -650,6 +685,15 @@ public class WebRTCWrapper {
return peerConnection;
}
public boolean applyDtmfTone(String tone) {
if (toneManager == null || peerConnection == null || localAudioTrack == null) {
return false;
}
localAudioTrack.rtpSender.dtmf().insertDtmf(tone, TONE_DURATION, 100);
toneManager.startTone(TONE_CODES.get(tone), TONE_DURATION);
return true;
}
@Nonnull
private PeerConnectionFactory requirePeerConnectionFactory() {
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;

View 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>

View file

@ -74,7 +74,6 @@
android:layout_width="match_parent"
android:layout_height="32dp"/>
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
@ -92,12 +91,21 @@
android:textAppearance="@style/TextAppearance.Conversations.Title.Monospace"
tools:text="01:23" />
<com.makeramen.roundedimageview.RoundedImageView
<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.google.android.material.imageview.ShapeableImageView
android:id="@+id/contact_photo"
android:layout_width="@dimen/publish_avatar_size"
android:layout_height="@dimen/publish_avatar_size"
android:layout_centerInParent="true"
app:riv_corner_radius="@dimen/incoming_call_radius" />
app:strokeColor="?colorAccent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.IncomingCall" />
</RelativeLayout>
@ -178,7 +186,7 @@
app:elevation="4dp"
app:fabCustomSize="72dp"
app:maxImageSize="36dp"
tools:visibility="visible" />
tools:visibility="gone" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/accept_call"
@ -193,7 +201,7 @@
app:elevation="4dp"
app:fabCustomSize="72dp"
app:maxImageSize="36dp"
tools:visibility="visible" />
tools:visibility="gone" />
</RelativeLayout>
@ -204,7 +212,7 @@
android:layout_centerVertical="true"
android:layout_margin="@dimen/in_call_fab_margin"
android:layout_toStartOf="@+id/end_call"
android:visibility="gone"
android:visibility="visible"
app:backgroundTint="?color_background_primary"
app:elevation="4dp"
app:fabSize="mini"
@ -230,7 +238,7 @@
android:layout_centerVertical="true"
android:layout_margin="@dimen/in_call_fab_margin"
android:layout_toEndOf="@+id/end_call"
android:visibility="gone"
android:visibility="visible"
app:backgroundTint="?color_background_primary"
app:elevation="4dp"
app:fabSize="mini"
@ -243,7 +251,7 @@
android:layout_centerVertical="true"
android:layout_margin="@dimen/in_call_fab_margin"
android:layout_toEndOf="@+id/in_call_action_right"
android:visibility="gone"
android:visibility="visible"
app:backgroundTint="?color_background_primary"
app:elevation="4dp"
app:fabSize="mini"

View 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="20sp" />
</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>

View file

@ -22,6 +22,11 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gateway_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/jid_layout"
android:layout_width="match_parent"
@ -38,5 +43,14 @@
android:imeOptions="actionDone|flagNoExtractUi"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/bookmark"
style="@style/Widget.Conversations.CheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="true"
android:text="Save as Contact / Bookmark"/>
</LinearLayout>
</layout>

View file

@ -0,0 +1,14 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingRight="5dp">
<ToggleButton
android:id="@+id/button"
android:gravity="center"
android:visibility="gone"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="?attr/edit_text_color"
android:textSize="?attr/TextSizeBody1" />
</LinearLayout>

View file

@ -8,6 +8,11 @@
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"

View file

@ -73,5 +73,8 @@
<dimen name="bigger_text_size">16sp</dimen>
<dimen name="big_text_size">18sp</dimen>
<dimen name="dialpad_text_size">30sp</dimen>
<dimen name="activity_margin">16dp</dimen>
<dimen name="colorpicker_hue_width">30dp</dimen>
</resources>

View file

@ -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_account">Manage account</string>

View file

@ -159,4 +159,20 @@
<style name="TextAppearance.Conversations.Body1.Secondary.OnDark" parent="TextAppearance.Conversations.Body1">
<item name="android:textColor">@color/white70</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>
<style name="ShapeAppearanceOverlay.IncomingCall" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">@dimen/incoming_call_radius</item>
</style>
</resources>