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.squareup.okhttp3:okhttp:4.10.0"
implementation 'com.google.guava:guava:31.1-android' 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 'im.conversations.webrtc:webrtc-android:104.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

View file

@ -2,10 +2,103 @@ package eu.siacs.conversations.utils;
import android.content.Context; import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.xmpp.Jid; 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 { public class PhoneNumberUtilWrapper {
public static String toFormattedPhoneNumber(Context context, Jid jid) {
throw new AssertionError("This method is not implemented in Conversations"); 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) {
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; 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() { public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
Map<String, String> typeMap = new HashMap<>(); Map<String, String> typeMap = new HashMap<>();
Map<String, String> nameMap = 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.Jid;
import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnBindListener;
import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnGatewayResult;
import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnMessageAcknowledged; import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
@ -3376,6 +3377,24 @@ public class XmppConnectionService extends Service {
fetchConferenceConfiguration(conversation, null); 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) { public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { 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) { public void fetchCaps(Account account, final Jid jid, final Presence presence) {
final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer()); final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); 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_jabber_id),
getString(R.string.block), getString(R.string.block),
null, null,
null,
account.getJid().asBareJid().toEscapedString(), account.getJid().asBareJid().toEscapedString(),
true, true,
false false,
EnterJidDialog.SanityCheck.NO
); );
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
Blockable blockable = new RawBlockable(account, contactJid); Blockable blockable = new RawBlockable(account, contactJid);
if (xmppConnectionService.sendBlockRequest(blockable, false)) { if (xmppConnectionService.sendBlockRequest(blockable, false)) {
Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show(); 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, mActivatedAccounts,
getString(R.string.enter_contact), getString(R.string.enter_contact),
getString(R.string.select), getString(R.string.select),
null,
jid == null ? null : jid.asBareJid().toString(), jid == null ? null : jid.asBareJid().toString(),
getIntent().getStringExtra(EXTRA_ACCOUNT), getIntent().getStringExtra(EXTRA_ACCOUNT),
true, true,
false false,
EnterJidDialog.SanityCheck.NO
); );
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> {
final Intent request = getIntent(); final Intent request = getIntent();
final Intent data = new Intent(); final Intent data = new Intent();
data.putExtra("contact", contactJid.toString()); data.putExtra("contact", contactJid.toString());

View file

@ -2,73 +2,106 @@ package eu.siacs.conversations.ui;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.EnterJidDialogBinding; import eu.siacs.conversations.databinding.EnterJidDialogBinding;
import eu.siacs.conversations.services.XmppConnectionService; 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.adapter.KnownHostsAdapter;
import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.ui.util.DelayedHintHelper; import eu.siacs.conversations.ui.util.DelayedHintHelper;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnGatewayResult;
public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher { public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher {
private static final List<String> SUSPICIOUS_DOMAINS = 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 OnEnterJidDialogPositiveListener mListener = null;
private static final String TITLE_KEY = "title"; private static final String TITLE_KEY = "title";
private static final String POSITIVE_BUTTON_KEY = "positive_button"; 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 PREFILLED_JID_KEY = "prefilled_jid";
private static final String ACCOUNT_KEY = "account"; private static final String ACCOUNT_KEY = "account";
private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid"; 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 ACCOUNTS_LIST_KEY = "activated_accounts_list";
private static final String SANITY_CHECK_JID = "sanity_check_jid"; private static final String SANITY_CHECK_JID = "sanity_check_jid";
private static final String SHOW_BOOKMARK_CHECKBOX = "show_bookmark_checkbox";
private KnownHostsAdapter knownHostsAdapter; private KnownHostsAdapter knownHostsAdapter;
private Collection<String> whitelistedDomains = Collections.emptyList(); private Collection<String> whitelistedDomains = Collections.emptyList();
private EnterJidDialogBinding binding; private EnterJidDialogBinding binding;
private AlertDialog dialog; private AlertDialog dialog;
private boolean sanityCheckJid = false; private SanityCheck sanityCheckJid = SanityCheck.NO;
private boolean issuedWarning = false; private boolean issuedWarning = false;
private GatewayListAdapter gatewayListAdapter = new GatewayListAdapter();
public static enum SanityCheck {
NO,
YES,
ALLOW_MUC
}
public static EnterJidDialog newInstance( public static EnterJidDialog newInstance(
final List<String> activatedAccounts, final List<String> activatedAccounts,
final String title, final String title,
final String positiveButton, final String positiveButton,
final String secondaryButton,
final String prefilledJid, final String prefilledJid,
final String account, final String account,
boolean allowEditJid, boolean allowEditJid,
final boolean sanity_check_jid) { boolean showBookmarkCheckbox,
final SanityCheck sanity_check_jid) {
EnterJidDialog dialog = new EnterJidDialog(); EnterJidDialog dialog = new EnterJidDialog();
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(TITLE_KEY, title); bundle.putString(TITLE_KEY, title);
bundle.putString(POSITIVE_BUTTON_KEY, positiveButton); bundle.putString(POSITIVE_BUTTON_KEY, positiveButton);
bundle.putString(SECONDARY_BUTTON_KEY, secondaryButton);
bundle.putString(PREFILLED_JID_KEY, prefilledJid); bundle.putString(PREFILLED_JID_KEY, prefilledJid);
bundle.putString(ACCOUNT_KEY, account); bundle.putString(ACCOUNT_KEY, account);
bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid); bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid);
bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) activatedAccounts); 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); dialog.setArguments(bundle);
return dialog; return dialog;
} }
@ -110,7 +143,11 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
binding.jid.setCursorVisible(false); 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); DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid);
@ -129,80 +166,153 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
binding.account.setAdapter(adapter); binding.account.setAdapter(adapter);
} }
builder.setView(binding.getRoot()); binding.gatewayList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
builder.setNegativeButton(R.string.cancel, null); binding.gatewayList.setAdapter(gatewayListAdapter);
builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); gatewayListAdapter.setOnEmpty(() -> binding.gatewayList.setVisibility(View.GONE));
this.dialog = builder.create(); gatewayListAdapter.setOnNonEmpty(() -> binding.gatewayList.setVisibility(View.VISIBLE));
View.OnClickListener dialogOnClick = binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
v -> { @Override
handleEnter(binding, account); 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( binding.jid.setOnEditorActionListener(
(v, actionId, event) -> { (v, actionId, event) -> {
handleEnter(binding, account); handleEnter(binding, account, false);
return true; return true;
}); });
dialog.show(); 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; return dialog;
} }
private void handleEnter(EnterJidDialogBinding binding, String account) { protected Jid accountJid() {
final 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) { if (!binding.account.isEnabled() && account == null) {
return; return;
} }
try { final Jid accountJid = accountJid();
if (Config.DOMAIN_LOCK != null) { final OnGatewayResult finish = (final String jidString, final String errorMessage) -> {
accountJid = Activity context = getActivity();
Jid.ofEscaped( if (context == null) return; // Race condition, we got the reply after the UI was closed
(String) binding.account.getSelectedItem(),
Config.DOMAIN_LOCK,
null);
} else {
accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem());
}
} catch (final IllegalArgumentException e) {
return;
}
final Jid contactJid;
try {
contactJid = Jid.ofEscaped(binding.jid.getText().toString().trim());
} catch (final IllegalArgumentException e) {
binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
return;
}
if (!issuedWarning && sanityCheckJid) { context.runOnUiThread(() -> {
if (contactJid.isDomainJid()) { if (errorMessage != null) {
binding.jidLayout.setError( binding.jidLayout.setError(errorMessage);
getActivity().getString(R.string.this_looks_like_a_domain)); return;
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));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway);
issuedWarning = true;
return;
}
}
if (mListener != null) {
try {
if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) {
dialog.dismiss();
} }
} catch (JidError error) { if (jidString == null) {
binding.jidLayout.setError(error.toString()); binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); return;
issuedWarning = false; }
}
Jid contactJid = null;
try {
contactJid = Jid.ofEscaped(jidString);
} catch (final IllegalArgumentException e) {
binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid));
return;
}
if (!issuedWarning && sanityCheckJid != SanityCheck.NO) {
if (contactJid.isDomainJid()) {
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 (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;
}
}
if (mListener != null) {
try {
if (mListener.onEnterJidDialogPositive(accountJid, contactJid, secondary, binding.bookmark.isChecked())) {
dialog.dismiss();
}
} catch (JidError error) {
binding.jidLayout.setError(error.toString());
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add);
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);
} }
} }
@ -244,7 +354,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
} }
public interface OnEnterJidDialogPositiveListener { 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 { public static class JidError extends Exception {
@ -276,4 +386,210 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
final String[] parts = domain.split("\\."); final String[] parts = domain.split("\\.");
return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); 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.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; 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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import org.jetbrains.annotations.NotNull;
import org.webrtc.RendererCommon; import org.webrtc.RendererCommon;
import org.webrtc.SurfaceViewRenderer; import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoTrack; import org.webrtc.VideoTrack;
@ -58,6 +58,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.XmppConnectionService; 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.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor; import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals; import eu.siacs.conversations.ui.util.Rationals;
@ -74,7 +75,7 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
public class RtpSessionActivity extends XmppActivity public class RtpSessionActivity extends XmppActivity
implements XmppConnectionService.OnJingleRtpConnectionUpdate, implements XmppConnectionService.OnJingleRtpConnectionUpdate,
eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
public static final String EXTRA_WITH = "with"; public static final String EXTRA_WITH = "with";
public static final String EXTRA_SESSION_ID = "session_id"; public static final String EXTRA_SESSION_ID = "session_id";
@ -86,7 +87,7 @@ public class RtpSessionActivity extends XmppActivity
private static final int CALL_DURATION_UPDATE_INTERVAL = 333; 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( Arrays.asList(
RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.SECURITY_ERROR, RtpEndUserState.SECURITY_ERROR,
@ -164,6 +165,17 @@ public class RtpSessionActivity extends XmppActivity
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
setSupportActionBar(binding.toolbar); 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 @Override
@ -171,10 +183,12 @@ public class RtpSessionActivity extends XmppActivity
getMenuInflater().inflate(R.menu.activity_rtp_session, menu); getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
final MenuItem help = menu.findItem(R.id.action_help); final MenuItem help = menu.findItem(R.id.action_help);
final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat); 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); final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
help.setVisible(Config.HELP != null && isHelpButtonVisible()); help.setVisible(Config.HELP != null && isHelpButtonVisible());
gotoChat.setVisible(isSwitchToConversationVisible()); gotoChat.setVisible(isSwitchToConversationVisible());
switchToVideo.setVisible(isSwitchToVideoVisible()); switchToVideo.setVisible(isSwitchToVideoVisible());
dialpad.setVisible(isAudioOnlyConversation());
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@ -212,6 +226,13 @@ public class RtpSessionActivity extends XmppActivity
&& STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); && 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() { private boolean isSwitchToVideoVisible() {
final JingleRtpConnection connection = final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
@ -229,6 +250,15 @@ public class RtpSessionActivity extends XmppActivity
switchToConversation(conversation); 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) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_help: case R.id.action_help:
@ -237,6 +267,9 @@ public class RtpSessionActivity extends XmppActivity
case R.id.action_goto_chat: case R.id.action_goto_chat:
switchToConversation(); switchToConversation();
return true; return true;
case R.id.action_dialpad:
toggleDialpadVisibility();
return true;
case R.id.action_switch_to_video: case R.id.action_switch_to_video:
requestPermissionAndSwitchToVideo(); requestPermissionAndSwitchToVideo();
return true; return true;
@ -407,26 +440,17 @@ public class RtpSessionActivity extends XmppActivity
private void putScreenInCallMode(final Set<Media> media) { private void putScreenInCallMode(final Set<Media> media) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (Media.audioOnly(media)) { if (!media.contains(Media.VIDEO)) {
final JingleRtpConnection rtpConnection = final JingleRtpConnection rtpConnection =
rtpConnectionReference != null ? rtpConnectionReference.get() : null; rtpConnectionReference != null ? rtpConnectionReference.get() : null;
final AppRTCAudioManager audioManager = final AppRTCAudioManager audioManager =
rtpConnection == null ? null : rtpConnection.getAudioManager(); rtpConnection == null ? null : rtpConnection.getAudioManager();
if (audioManager == null if (audioManager == null
|| audioManager.getSelectedAudioDevice() || audioManager.getSelectedAudioDevice()
== AppRTCAudioManager.AudioDevice.EARPIECE) { == AppRTCAudioManager.AudioDevice.EARPIECE) {
acquireProximityWakeLock(); 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") @SuppressLint("WakelockTimeout")
@ -543,8 +567,8 @@ public class RtpSessionActivity extends XmppActivity
} }
if (END_CARD.contains(state) if (END_CARD.contains(state)
|| xmppConnectionService || xmppConnectionService
.getJingleConnectionManager() .getJingleConnectionManager()
.hasMatchingProposal(account, with)) { .hasMatchingProposal(account, with)) {
return; return;
} }
Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing"); Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
@ -552,7 +576,7 @@ public class RtpSessionActivity extends XmppActivity
} }
} }
private void setWidth(final RtpEndUserState state) { private void setWith(final RtpEndUserState state) {
setWith(getWith(), state); setWith(getWith(), state);
} }
@ -738,10 +762,10 @@ public class RtpSessionActivity extends XmppActivity
final JingleRtpConnection rtpConnection = requireRtpConnection(); final JingleRtpConnection rtpConnection = requireRtpConnection();
return rtpConnection.getMedia().contains(Media.VIDEO) return rtpConnection.getMedia().contains(Media.VIDEO)
&& Arrays.asList( && Arrays.asList(
RtpEndUserState.ACCEPTING_CALL, RtpEndUserState.ACCEPTING_CALL,
RtpEndUserState.CONNECTING, RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED) RtpEndUserState.CONNECTED)
.contains(rtpConnection.getEndUserState()); .contains(rtpConnection.getEndUserState());
} catch (final IllegalStateException e) { } catch (final IllegalStateException e) {
return false; return false;
} }
@ -759,8 +783,9 @@ public class RtpSessionActivity extends XmppActivity
.getJingleConnectionManager() .getJingleConnectionManager()
.getTerminalSessionState(with, sessionId); .getTerminalSessionState(with, sessionId);
if (terminatedRtpSession == null) { if (terminatedRtpSession == null) {
throw new IllegalStateException( Log.e(Config.LOGTAG, "failed to initialize activity with running rtp session. session not found");
"failed to initialize activity with running rtp session. session not found"); finish();
return true;
} }
initializeWithTerminatedSessionState(account, with, terminatedRtpSession); initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
return true; return true;
@ -781,7 +806,7 @@ public class RtpSessionActivity extends XmppActivity
requireRtpConnection().getState())) { requireRtpConnection().getState())) {
putScreenInCallMode(); putScreenInCallMode();
} }
setWidth(currentState); setWith(currentState);
updateVideoViews(currentState); updateVideoViews(currentState);
updateStateDisplay(currentState, media, contentAddition); updateStateDisplay(currentState, media, contentAddition);
updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
@ -992,11 +1017,11 @@ public class RtpSessionActivity extends XmppActivity
this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp); this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp);
this.binding.acceptCall.setVisibility(View.VISIBLE); this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (asList( } else if (asList(
RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR, RtpEndUserState.CONNECTIVITY_LOST_ERROR,
RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.RETRACTED, RtpEndUserState.RETRACTED,
RtpEndUserState.SECURITY_ERROR) RtpEndUserState.SECURITY_ERROR)
.contains(state)) { .contains(state)) {
this.binding.rejectCall.setContentDescription(getString(R.string.exit)); this.binding.rejectCall.setContentDescription(getString(R.string.exit));
this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setOnClickListener(this::exit);
@ -1389,7 +1414,6 @@ public class RtpSessionActivity extends XmppActivity
final AbstractJingleConnection.Id id = requireRtpConnection().getId(); final AbstractJingleConnection.Id id = requireRtpConnection().getId();
final boolean verified = requireRtpConnection().isVerified(); final boolean verified = requireRtpConnection().isVerified();
final Set<Media> media = getMedia(); final Set<Media> media = getMedia();
lockOrientation(media);
final ContentAddition contentAddition = getPendingContentAddition(); final ContentAddition contentAddition = getPendingContentAddition();
final Contact contact = getWith(); final Contact contact = getWith();
if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { 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( private void updateRtpSessionProposalState(
final Account account, final Jid with, final RtpEndUserState state) { final Account account, final Jid with, final RtpEndUserState state) {
final Intent currentIntent = getIntent(); final Intent currentIntent = getIntent();

View file

@ -517,15 +517,17 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
ft.addToBackStack(null); ft.addToBackStack(null);
EnterJidDialog dialog = EnterJidDialog.newInstance( EnterJidDialog dialog = EnterJidDialog.newInstance(
mActivatedAccounts, mActivatedAccounts,
getString(R.string.add_contact), getString(R.string.start_conversation),
getString(R.string.add), getString(R.string.message),
"Call",
prefilledJid, prefilledJid,
invite == null ? null : invite.account, invite == null ? null : invite.account,
invite == null || !invite.hasFingerprints(), invite == null || !invite.hasFingerprints(),
true true,
EnterJidDialog.SanityCheck.ALLOW_MUC
); );
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, call, save) -> {
if (!xmppConnectionServiceBound) { if (!xmppConnectionServiceBound) {
return false; return false;
} }
@ -534,25 +536,57 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (account == null) { if (account == null) {
return true; return true;
} }
final Contact contact = account.getRoster().getContact(contactJid); final Contact contact = account.getRoster().getContact(contactJid);
if (invite != null && invite.getName() != null) { if (invite != null && invite.getName() != null) {
contact.setServerName(invite.getName()); contact.setServerName(invite.getName());
} }
if (contact.isSelf()) {
switchToConversation(contact); if (contact.isSelf() || contact.showInRoster()) {
return true; switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
} else if (contact.showInRoster()) {
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
} else {
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; return true;
} }
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(), call ? "call" : null);
}
try {
dialog.dismiss();
} catch (final IllegalStateException e) { }
});
return false;
}); });
dialog.show(ft, FRAGMENT_TAG_DIALOG); 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); 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) { public void highlightInMuc(Conversation conversation, String nick) {
switchToConversation(conversation, null, false, nick, false, false); switchToConversation(conversation, null, false, nick, false, false);
} }
@ -559,7 +564,13 @@ public abstract class XmppActivity extends ActionBarActivity {
switchToConversation(conversation, null, false, nick, true, false); 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 intent = new Intent(this, ConversationsActivity.class);
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
@ -576,6 +587,7 @@ public abstract class XmppActivity extends ActionBarActivity {
if (doNotAppend) { if (doNotAppend) {
intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true); 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); intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent); startActivity(intent);
finish(); 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 final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0);
public static String getUserCountry(final Context context) { public static String getUserCountry(final Context context) {
return getUserCountry(context, false);
}
public static String getUserCountry(final Context context, boolean preferNetwork) {
try { try {
final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class); final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class);
if (tm == null) { if (tm == null) {
return getUserCountryFallback(); return getUserCountryFallback();
} }
final String simCountry = tm.getSimCountryIso(); 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 if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
return simCountry.toUpperCase(Locale.US); return simCountry.toUpperCase(Locale.US);
} else if (tm.getPhoneType() != TelephonyManager.PHONE_TYPE_CDMA) { // device is not 3G (would be unreliable) } else if (networkCountry != null && networkCountry.length() == 2) { // network country code is available
String networkCountry = tm.getNetworkCountryIso(); return networkCountry.toUpperCase(Locale.US);
if (networkCountry != null && networkCountry.length() == 2) { // network country code is available
return networkCountry.toUpperCase(Locale.US);
}
} }
return getUserCountryFallback(); return getUserCountryFallback();
} catch (final Exception e) { } catch (final Exception e) {
@ -72,4 +78,4 @@ public class LocationProvider {
return FALLBACK; return FALLBACK;
} }
} }

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) { private void receiveSessionTerminate(final JinglePacket jinglePacket) {
respondOk(jinglePacket); respondOk(jinglePacket);
final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();

View file

@ -148,7 +148,7 @@ class ToneManager {
private void scheduleWaitingTone() { private void scheduleWaitingTone() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { 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); }, 0, 3, TimeUnit.SECONDS);
} }
@ -161,14 +161,16 @@ class ToneManager {
currentTone.cancel(true); currentTone.cancel(true);
} }
if (toneGenerator != null) { if (toneGenerator != null) {
toneGenerator.stopTone(); // 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) { if (this.toneGenerator != null) {
this.toneGenerator.release();; this.toneGenerator.release();
} }
final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class); final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class);
final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL; final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.content.Context; import android.content.Context;
import android.media.ToneGenerator;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -8,6 +9,7 @@ import android.util.Log;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -20,6 +22,7 @@ import org.webrtc.CandidatePairChangeEvent;
import org.webrtc.DataChannel; import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory; import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory; import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.DtmfSender;
import org.webrtc.EglBase; import org.webrtc.EglBase;
import org.webrtc.IceCandidate; import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints; import org.webrtc.MediaConstraints;
@ -38,6 +41,7 @@ import org.webrtc.voiceengine.WebRtcAudioEffects;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -57,6 +61,25 @@ public class WebRTCWrapper {
private final ExecutorService executorService = Executors.newSingleThreadExecutor(); 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 = private static final Set<String> HARDWARE_AEC_BLACKLIST =
new ImmutableSet.Builder<String>() new ImmutableSet.Builder<String>()
.add("Pixel") .add("Pixel")
@ -510,8 +533,14 @@ public class WebRTCWrapper {
} }
boolean isMicrophoneEnabled() { boolean isMicrophoneEnabled() {
final Optional<AudioTrack> audioTrack = Optional<AudioTrack> audioTrack = null;
TrackWrapper.get(peerConnection, this.localAudioTrack); 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()) { if (audioTrack.isPresent()) {
try { try {
return audioTrack.get().enabled(); return audioTrack.get().enabled();
@ -526,8 +555,14 @@ public class WebRTCWrapper {
} }
boolean setMicrophoneEnabled(final boolean enabled) { boolean setMicrophoneEnabled(final boolean enabled) {
final Optional<AudioTrack> audioTrack = Optional<AudioTrack> audioTrack = null;
TrackWrapper.get(peerConnection, this.localAudioTrack); 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()) { if (audioTrack.isPresent()) {
try { try {
audioTrack.get().setEnabled(enabled); audioTrack.get().setEnabled(enabled);
@ -650,6 +685,15 @@ public class WebRTCWrapper {
return peerConnection; 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 @Nonnull
private PeerConnectionFactory requirePeerConnectionFactory() { private PeerConnectionFactory requirePeerConnectionFactory() {
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; 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_width="match_parent"
android:layout_height="32dp"/> android:layout_height="32dp"/>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<RelativeLayout <RelativeLayout
@ -92,12 +91,21 @@
android:textAppearance="@style/TextAppearance.Conversations.Title.Monospace" android:textAppearance="@style/TextAppearance.Conversations.Title.Monospace"
tools:text="01:23" /> 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:id="@+id/contact_photo"
android:layout_width="@dimen/publish_avatar_size" android:layout_width="@dimen/publish_avatar_size"
android:layout_height="@dimen/publish_avatar_size" android:layout_height="@dimen/publish_avatar_size"
android:layout_centerInParent="true" android:layout_centerInParent="true"
app:riv_corner_radius="@dimen/incoming_call_radius" /> app:strokeColor="?colorAccent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.IncomingCall" />
</RelativeLayout> </RelativeLayout>
@ -178,7 +186,7 @@
app:elevation="4dp" app:elevation="4dp"
app:fabCustomSize="72dp" app:fabCustomSize="72dp"
app:maxImageSize="36dp" app:maxImageSize="36dp"
tools:visibility="visible" /> tools:visibility="gone" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/accept_call" android:id="@+id/accept_call"
@ -193,7 +201,7 @@
app:elevation="4dp" app:elevation="4dp"
app:fabCustomSize="72dp" app:fabCustomSize="72dp"
app:maxImageSize="36dp" app:maxImageSize="36dp"
tools:visibility="visible" /> tools:visibility="gone" />
</RelativeLayout> </RelativeLayout>
@ -204,7 +212,7 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_margin="@dimen/in_call_fab_margin" android:layout_margin="@dimen/in_call_fab_margin"
android:layout_toStartOf="@+id/end_call" android:layout_toStartOf="@+id/end_call"
android:visibility="gone" android:visibility="visible"
app:backgroundTint="?color_background_primary" app:backgroundTint="?color_background_primary"
app:elevation="4dp" app:elevation="4dp"
app:fabSize="mini" app:fabSize="mini"
@ -230,7 +238,7 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_margin="@dimen/in_call_fab_margin" android:layout_margin="@dimen/in_call_fab_margin"
android:layout_toEndOf="@+id/end_call" android:layout_toEndOf="@+id/end_call"
android:visibility="gone" android:visibility="visible"
app:backgroundTint="?color_background_primary" app:backgroundTint="?color_background_primary"
app:elevation="4dp" app:elevation="4dp"
app:fabSize="mini" app:fabSize="mini"
@ -243,7 +251,7 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_margin="@dimen/in_call_fab_margin" android:layout_margin="@dimen/in_call_fab_margin"
android:layout_toEndOf="@+id/in_call_action_right" android:layout_toEndOf="@+id/in_call_action_right"
android:visibility="gone" android:visibility="visible"
app:backgroundTint="?color_background_primary" app:backgroundTint="?color_background_primary"
app:elevation="4dp" app:elevation="4dp"
app:fabSize="mini" 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_width="fill_parent"
android:layout_height="wrap_content"/> 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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/jid_layout" android:id="@+id/jid_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -38,5 +43,14 @@
android:imeOptions="actionDone|flagNoExtractUi" android:imeOptions="actionDone|flagNoExtractUi"
android:inputType="textEmailAddress" /> android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout> </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> </LinearLayout>
</layout> </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:icon="?attr/icon_help"
android:title="@string/help" android:title="@string/help"
app:showAsAction="always" /> 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 <item
android:id="@+id/action_goto_chat" android:id="@+id/action_goto_chat"
android:icon="?attr/icon_goto_chat" android:icon="?attr/icon_goto_chat"

View file

@ -73,5 +73,8 @@
<dimen name="bigger_text_size">16sp</dimen> <dimen name="bigger_text_size">16sp</dimen>
<dimen name="big_text_size">18sp</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> <dimen name="colorpicker_hue_width">30dp</dimen>
</resources> </resources>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_dialpad">Dialpad</string>
<string name="action_add">New conversation</string> <string name="action_add">New conversation</string>
<string name="action_accounts">Manage accounts</string> <string name="action_accounts">Manage accounts</string>
<string name="action_account">Manage account</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"> <style name="TextAppearance.Conversations.Body1.Secondary.OnDark" parent="TextAppearance.Conversations.Body1">
<item name="android:textColor">@color/white70</item> <item name="android:textColor">@color/white70</item>
</style> </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> </resources>