Add ability to add custom nodes

This commit is contained in:
pokkst 2022-09-17 04:14:15 -05:00
parent 1e4a91d046
commit 4657132067
No known key found for this signature in database
GPG key ID: 90C2ED85E67A50FF
15 changed files with 702 additions and 179 deletions

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2017 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.xmrwallet.adapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.DefaultNodes;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.util.Constants;
import java.util.ArrayList;
import java.util.List;
public class NodeSelectionAdapter extends RecyclerView.Adapter<NodeSelectionAdapter.ViewHolder> {
private List<Node> localDataSet;
private NodeSelectionAdapterListener listener = null;
/**
* Initialize the dataset of the Adapter.
*/
public NodeSelectionAdapter(NodeSelectionAdapterListener listener) {
this.listener = listener;
this.localDataSet = new ArrayList<>();
}
public void submitList(List<Node> dataSet) {
this.localDataSet = dataSet;
notifyDataSetChanged();
}
public void updateSelectedNode() {
notifyDataSetChanged();
}
// Create new views (invoked by the layout manager)
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Create a new view, which defines the UI of the list item
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.node_selection_item, viewGroup, false);
return new ViewHolder(listener, view);
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
Node node = localDataSet.get(position);
viewHolder.bind(node);
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return localDataSet.size();
}
public interface NodeSelectionAdapterListener {
void onSelectNode(Node node);
}
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
private final NodeSelectionAdapterListener listener;
public ViewHolder(NodeSelectionAdapterListener listener, View view) {
super(view);
this.listener = listener;
}
public void bind(Node node) {
String currentNodeString = PrefService.getInstance().getString(Constants.PREF_NODE, DefaultNodes.XMRTW.getAddress());
Node currentNode = Node.fromString(currentNodeString);
boolean match = node.equals(currentNode);
if(match) {
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.oled_colorSecondary));
} else {
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
}
TextView nodeNameTextView = itemView.findViewById(R.id.node_name_textview);
TextView nodeAddressTextView = itemView.findViewById(R.id.node_uri_textview);
nodeNameTextView.setText(node.getName());
nodeAddressTextView.setText(node.getAddress());
itemView.setOnClickListener(view -> listener.onSelectNode(node));
}
}
}

View file

@ -16,25 +16,34 @@
package com.m2049r.xmrwallet.data;
import lombok.AllArgsConstructor;
import lombok.Getter;
// Nodes stolen from https://moneroworld.com/#nodes
@AllArgsConstructor
public enum DefaultNodes {
MONERUJO("nodex.monerujo.io:18081"),
XMRTO("node.xmr.to:18081"),
SUPPORTXMR("node.supportxmr.com:18081"),
HASHVAULT("nodes.hashvault.pro:18081"),
MONEROWORLD("node.moneroworld.com:18089"),
XMRTW("opennode.xmr-tw.org:18089"),
MONERUJO("nodex.monerujo.io:18081/mainnet/monerujo"),
SUPPORTXMR("node.supportxmr.com:18081/mainnet/SupportXMR"),
HASHVAULT("nodes.hashvault.pro:18081/mainnet/Hashvault"),
MONEROWORLD("node.moneroworld.com:18089/mainnet/MoneroWorld"),
XMRTW("opennode.xmr-tw.org:18089/mainnet/XMRTW"),
MONERUJO_ONION("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"),
Criminales78("56wl7y2ebhamkkiza4b7il4mrzwtyvpdym7bm2bkg3jrei2je646k3qd.onion:18089/mainnet/Criminales78.onion"),
xmrfail("mxcd4577fldb3ppzy7obmmhnu3tf57gbcbd4qhwr2kxyjj2qi3dnbfqd.onion:18081/mainnet/xmrfail.onion"),
boldsuck("6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081/mainnet/boldsuck.onion"),
SAMOURAI("446unwib5vc7pfbzflosy6m6vtyuhddnalr3hutyavwe4esfuu5g6ryd.onion:18089/mainnet/samourai.onion");
@Getter
private final String uri;
DefaultNodes(String uri) {
this.uri = uri;
}
public String getUri() {
return uri;
}
public String getAddress() {
return uri.split("/")[0];
}
public String getName() {
return uri.split("/")[2];
}
}

View file

@ -42,7 +42,6 @@ public class Node {
@Getter
@Setter
private final boolean selected = false;
Address hostAddress;
@Getter
@Setter
int rpcPort = 0;
@ -151,7 +150,6 @@ public class Node {
// constructor used for created nodes from retrieved peer lists
public Node(InetSocketAddress socketAddress) {
this();
this.hostAddress = Address.of(socketAddress.getAddress());
this.host = socketAddress.getHostString();
this.rpcPort = 0; // unknown
this.levinPort = socketAddress.getPort();
@ -213,7 +211,7 @@ public class Node {
@Override
public int hashCode() {
return hostAddress.hashCode();
return host.hashCode();
}
// Nodes are equal if they are the same host address:port & are on the same network
@ -221,13 +219,14 @@ public class Node {
public boolean equals(Object other) {
if (!(other instanceof Node)) return false;
final Node anotherNode = (Node) other;
return (hostAddress.equals(anotherNode.hostAddress)
return (host.equals(anotherNode.host)
&& (getAddress().equals(anotherNode.getAddress()))
&& (rpcPort == anotherNode.rpcPort)
&& (networkType == anotherNode.networkType));
}
public boolean isOnion() {
return hostAddress.isOnion();
return OnionHelper.isOnionHost(host);
}
public String toNodeString() {
@ -263,49 +262,23 @@ public class Node {
}
public String getAddress() {
return getHostAddress() + ":" + rpcPort;
return getHost() + ":" + rpcPort;
}
public String getHostAddress() {
return hostAddress.getHostAddress();
public String getHost() {
return host;
}
public void setHost(String host) throws UnknownHostException {
if ((host == null) || (host.isEmpty()))
throw new UnknownHostException("loopback not supported (yet?)");
this.host = host;
this.hostAddress = Address.of(host);
}
public void setDefaultName() {
if (name != null) return;
String nodeName = hostAddress.getHostName();
if (hostAddress.isOnion()) {
nodeName = nodeName.substring(0, nodeName.length() - ".onion".length());
if (nodeName.length() > 16) {
nodeName = nodeName.substring(0, 8) + "" + nodeName.substring(nodeName.length() - 6);
}
nodeName = nodeName + ".onion";
}
this.name = nodeName;
}
public void setName(String name) {
if ((name == null) || (name.isEmpty()))
setDefaultName();
else
this.name = name;
}
public void toggleFavourite() {
favourite = !favourite;
}
public void overwriteWith(Node anotherNode) {
if (networkType != anotherNode.networkType)
throw new IllegalStateException("network types do not match");
name = anotherNode.name;
hostAddress = anotherNode.hostAddress;
host = anotherNode.host;
rpcPort = anotherNode.rpcPort;
levinPort = anotherNode.levinPort;

View file

@ -0,0 +1,80 @@
package com.m2049r.xmrwallet.fragment.dialog;
import android.os.Bundle;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.util.Constants;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class AddNodeBottomSheetDialog extends BottomSheetDialogFragment {
public AddNodeListener listener = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.add_node_bottom_sheet_dialog, null);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Button addNodeButton = view.findViewById(R.id.add_node_button);
EditText addressEditText = view.findViewById(R.id.address_edittext);
EditText nodeNameEditText = view.findViewById(R.id.node_name_edittext);
addNodeButton.setOnClickListener(view1 -> {
String node = addressEditText.getText().toString();
String name = nodeNameEditText.getText().toString();
if(node.contains(":") && !name.isEmpty()) {
String[] nodeParts = node.split(":");
if(nodeParts.length == 2) {
try {
String address = nodeParts[0];
int port = Integer.parseInt(nodeParts[1]);
String newNodeString = address + ":" + port + "/mainnet/" + name;
boolean validIp = Patterns.IP_ADDRESS.matcher(address).matches();
if(validIp) {
String nodesArray = PrefService.getInstance().getString(Constants.PREF_CUSTOM_NODES, "[]");
JSONArray jsonArray = new JSONArray(nodesArray);
boolean exists = false;
for(int i = 0; i < jsonArray.length(); i++) {
String nodeString = jsonArray.getString(i);
if(nodeString.equals(newNodeString))
exists = true;
}
if(!exists) {
jsonArray.put(newNodeString);
}
PrefService.getInstance().edit().putString(Constants.PREF_CUSTOM_NODES, jsonArray.toString()).apply();
if(listener != null) {
listener.onNodeAdded();
}
dismiss();
}
} catch(NumberFormatException | JSONException e) {
e.printStackTrace();
}
}
}
});
}
public interface AddNodeListener {
void onNodeAdded();
}
}

View file

@ -0,0 +1,89 @@
package com.m2049r.xmrwallet.fragment.dialog;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.adapter.NodeSelectionAdapter;
import com.m2049r.xmrwallet.data.DefaultNodes;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.util.Constants;
import com.m2049r.xmrwallet.util.Helper;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Collections;
public class NodeSelectionBottomSheetDialog extends BottomSheetDialogFragment implements NodeSelectionAdapter.NodeSelectionAdapterListener {
private NodeSelectionAdapter adapter = null;
public NodeSelectionDialogListener listener = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.node_selection_bottom_sheet_dialog, null);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ArrayList<Node> nodes = new ArrayList<>();
adapter = new NodeSelectionAdapter(this);
RecyclerView recyclerView = view.findViewById(R.id.node_selection_recyclerview);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(adapter);
Button addNodeButton = view.findViewById(R.id.add_node_button);
addNodeButton.setOnClickListener(view1 -> {
if(listener != null) {
listener.onClickedAddNode();
}
dismiss();
});
for(DefaultNodes defaultNode : DefaultNodes.values()) {
nodes.add(Node.fromString(defaultNode.getUri()));
}
try {
String nodesArray = PrefService.getInstance().getString(Constants.PREF_CUSTOM_NODES, "[]");
JSONArray jsonArray = new JSONArray(nodesArray);
for(int i = 0; i < jsonArray.length(); i++) {
String nodeString = jsonArray.getString(i);
Node node = Node.fromString(nodeString);
nodes.add(node);
}
} catch (JSONException e) {
e.printStackTrace();
}
adapter.submitList(nodes);
}
@Override
public void onSelectNode(Node node) {
PrefService.getInstance().edit().putString(Constants.PREF_NODE, node.getAddress()).apply();
WalletManager.getInstance().setDaemon(node);
adapter.updateSelectedNode();
}
public interface NodeSelectionDialogListener {
void onClickedAddNode();
}
}

View file

@ -9,6 +9,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -16,18 +17,25 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.DefaultNodes;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.fragment.dialog.AddNodeBottomSheetDialog;
import com.m2049r.xmrwallet.fragment.dialog.InformationBottomSheetDialog;
import com.m2049r.xmrwallet.fragment.dialog.NodeSelectionBottomSheetDialog;
import com.m2049r.xmrwallet.fragment.dialog.PasswordBottomSheetDialog;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.BlockchainService;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.util.Constants;
import com.m2049r.xmrwallet.util.DayNightMode;
import com.m2049r.xmrwallet.util.NightmodeHelper;
public class SettingsFragment extends Fragment implements PasswordBottomSheetDialog.PasswordListener {
public class SettingsFragment extends Fragment implements PasswordBottomSheetDialog.PasswordListener, NodeSelectionBottomSheetDialog.NodeSelectionDialogListener, AddNodeBottomSheetDialog.AddNodeListener {
private SettingsViewModel mViewModel;
TextWatcher proxyAddressListener = new TextWatcher() {
@ -64,6 +72,7 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia
super.onViewCreated(view, savedInstanceState);
mViewModel = new ViewModelProvider(this).get(SettingsViewModel.class);
Button displaySeedButton = view.findViewById(R.id.display_seed_button);
Button selectNodeButton = view.findViewById(R.id.select_node_button);
SwitchCompat nightModeSwitch = view.findViewById(R.id.day_night_switch);
SwitchCompat torSwitch = view.findViewById(R.id.tor_switch);
ConstraintLayout proxySettingsLayout = view.findViewById(R.id.wallet_proxy_settings_layout);
@ -126,6 +135,24 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia
displaySeedDialog();
}
});
TextView statusTextView = view.findViewById(R.id.status_textview);
BlockchainService.getInstance().connectionStatus.observe(getViewLifecycleOwner(), connectionStatus -> {
if(connectionStatus == Wallet.ConnectionStatus.ConnectionStatus_Connected) {
statusTextView.setText(getResources().getText(R.string.connected));
} else if(connectionStatus == Wallet.ConnectionStatus.ConnectionStatus_Disconnected) {
statusTextView.setText(getResources().getText(R.string.disconnected));
} else if(connectionStatus == Wallet.ConnectionStatus.ConnectionStatus_WrongVersion) {
statusTextView.setText(getResources().getText(R.string.version_mismatch));
}
});
Node node = Node.fromString(PrefService.getInstance().getString(Constants.PREF_NODE, DefaultNodes.XMRTW.getAddress()));
selectNodeButton.setText(getString(R.string.node_button_text, node.getAddress()));
selectNodeButton.setOnClickListener(view1 -> {
NodeSelectionBottomSheetDialog dialog = new NodeSelectionBottomSheetDialog();
dialog.listener = this;
dialog.show(getActivity().getSupportFragmentManager(), "node_selection_dialog");
});
}
private void displaySeedDialog() {
@ -164,4 +191,18 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia
walletProxyAddressEditText.addTextChangedListener(proxyAddressListener);
walletProxyPortEditText.addTextChangedListener(proxyPortListener);
}
@Override
public void onClickedAddNode() {
AddNodeBottomSheetDialog addNodeDialog = new AddNodeBottomSheetDialog();
addNodeDialog.listener = this;
addNodeDialog.show(getActivity().getSupportFragmentManager(), "add_node_dialog");
}
@Override
public void onNodeAdded() {
NodeSelectionBottomSheetDialog dialog = new NodeSelectionBottomSheetDialog();
dialog.listener = this;
dialog.show(getActivity().getSupportFragmentManager(), "node_selection_dialog");
}
}

View file

@ -8,6 +8,7 @@ import android.widget.Toast;
import androidx.lifecycle.ViewModel;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.DefaultNodes;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.PrefService;
import com.m2049r.xmrwallet.service.TxService;
@ -20,7 +21,10 @@ public class SettingsViewModel extends ViewModel {
public void updateProxy() {
AsyncTask.execute(() -> {
boolean usesProxy = PrefService.getInstance().getBoolean(Constants.PREF_USES_TOR, false);
if(!usesProxy) {
String currentNodeString = PrefService.getInstance().getString(Constants.PREF_NODE, DefaultNodes.XMRTW.getAddress());
boolean isNodeLocalIp = currentNodeString.startsWith("10.") || currentNodeString.startsWith("192.168.") || currentNodeString.equals("localhost") || currentNodeString.equals("127.0.0.1");
if(!usesProxy || isNodeLocalIp) {
WalletManager.getInstance().setProxy("");
WalletManager.getInstance().getWallet().setProxy("");
return;
@ -29,6 +33,7 @@ public class SettingsViewModel extends ViewModel {
if(proxyAddress.isEmpty()) proxyAddress = "127.0.0.1";
if(proxyPort.isEmpty()) proxyPort = "9050";
boolean validIpAddress = Patterns.IP_ADDRESS.matcher(proxyAddress).matches();
if(validIpAddress) {
String proxy = proxyAddress + ":" + proxyPort;
PrefService.getInstance().edit().putString(Constants.PREF_PROXY, proxy).apply();

View file

@ -3,12 +3,15 @@ package com.m2049r.xmrwallet.service;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
public class BlockchainService extends ServiceBase {
public static BlockchainService instance = null;
private final MutableLiveData<Long> _currentHeight = new MutableLiveData<>(0L);
public LiveData<Long> height = _currentHeight;
private final MutableLiveData<Wallet.ConnectionStatus> _connectionStatus = new MutableLiveData<>(Wallet.ConnectionStatus.ConnectionStatus_Disconnected);
public LiveData<Wallet.ConnectionStatus> connectionStatus = _connectionStatus;
private long daemonHeight = 0;
private long lastDaemonHeightUpdateTimeMs = 0;
public BlockchainService(MoneroHandlerThread thread) {
@ -44,4 +47,8 @@ public class BlockchainService extends ServiceBase {
}
}
}
public void setConnectionStatus(Wallet.ConnectionStatus status) {
_connectionStatus.postValue(status);
}
}

View file

@ -55,15 +55,16 @@ public class MoneroHandlerThread extends Thread implements WalletListener {
@Override
public void run() {
String currentNodeString = PrefService.getInstance().getString(Constants.PREF_NODE, DefaultNodes.XMRTW.getAddress());
Node selectedNode = Node.fromString(currentNodeString);
boolean usesTor = PrefService.getInstance().getBoolean(Constants.PREF_USES_TOR, false);
if (usesTor) {
boolean isLocalIp = currentNodeString.startsWith("10.") || currentNodeString.startsWith("192.168.") || currentNodeString.equals("localhost") || currentNodeString.equals("127.0.0.1");
if (usesTor && !isLocalIp) {
String proxy = PrefService.getInstance().getString(Constants.PREF_PROXY, "");
WalletManager.getInstance().setProxy(proxy);
WalletManager.getInstance().setDaemon(Node.fromString(DefaultNodes.SAMOURAI.getUri()));
wallet.setProxy(proxy);
} else {
WalletManager.getInstance().setDaemon(Node.fromString(DefaultNodes.XMRTW.getUri()));
}
WalletManager.getInstance().setDaemon(selectedNode);
wallet.init(0);
wallet.setListener(this);
wallet.startRefresh();
@ -108,6 +109,8 @@ public class MoneroHandlerThread extends Thread implements WalletListener {
wallet.store();
refresh();
}
BlockchainService.getInstance().setConnectionStatus(status);
}
private void refresh() {

View file

@ -7,6 +7,8 @@ public class Constants {
public static final String PREF_USES_TOR = "pref_uses_tor";
public static final String PREF_NIGHT_MODE = "pref_night_mode";
public static final String PREF_PROXY = "pref_proxy";
public static final String PREF_NODE = "pref_node";
public static final String PREF_CUSTOM_NODES = "pref_custom_nodes";
public static final String URI_PREFIX = "monero:";
public static final String URI_ARG_AMOUNT = "tx_amount";

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:fitsSystemWindows="true"
android:padding="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- CREATE LAYOUT -->
<TextView
android:id="@+id/send_monero_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="@string/add_node"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/node_name_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/node_name_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:background="@drawable/edittext_bg"
android:ellipsize="middle"
android:hint="@string/node_name_hint"
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@id/address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/send_monero_textview" />
<ImageButton
android:id="@+id/paste_address_imagebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@android:color/transparent"
android:minWidth="24dp"
android:minHeight="24dp"
android:padding="8dp"
android:src="@drawable/ic_content_paste_24dp"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/address_edittext"
app:layout_constraintTop_toTopOf="@id/address_edittext"
tools:ignore="SpeakableTextPresentCheck" />
<EditText
android:id="@+id/address_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="@drawable/edittext_bg"
android:hint="@string/node_address_hint"
android:inputType="text"
android:digits="1234567890.:"
app:layout_constraintBottom_toTopOf="@id/add_node_button"
app:layout_constraintEnd_toStartOf="@id/paste_address_imagebutton"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/add_node_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@drawable/button_bg"
android:text="@string/add_node"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/address_edittext" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -1,24 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context=".fragment.settings.SettingsFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/settings_textview"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/settings"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/wallet_settings_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/status_textview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/status_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/disconnected"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/settings_textview"
app:layout_constraintBottom_toBottomOf="@id/settings_textview"
app:layout_constraintTop_toTopOf="@id/settings_textview" />
<TextView
android:id="@+id/wallet_settings_textview"
android:layout_width="match_parent"
@ -27,19 +40,17 @@
android:text="@string/wallet"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/display_seed_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/settings_textview" />
<Button
android:id="@+id/display_seed_button"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/button_bg"
android:text="@string/display_recovery_phrase"
app:layout_constraintBottom_toTopOf="@id/appearance_settings_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
@ -53,7 +64,6 @@
android:text="@string/appearance"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/day_night_switch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/display_seed_button" />
@ -74,7 +84,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/network_settings_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appearance_settings_textview" />
@ -86,11 +95,23 @@
android:text="@string/network"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tor_switch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/day_night_switch" />
<Button
android:id="@+id/select_node_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:singleLine="true"
android:ellipsize="middle"
android:background="@drawable/button_bg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/network_settings_textview" />
<TextView
android:id="@+id/tor_textview"
android:layout_width="0dp"
@ -108,7 +129,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/network_settings_textview" />
app:layout_constraintTop_toBottomOf="@id/select_node_button" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/wallet_proxy_settings_layout"
@ -143,4 +164,5 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_proxy_address_edittext" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:padding="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/nodes_textview"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/nodes"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/add_node_button"
app:layout_constraintEnd_toStartOf="@id/add_node_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/add_node_button" />
<Button
android:id="@+id/add_node_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_bg"
android:text="@string/add_node"
app:layout_constraintStart_toEndOf="@id/nodes_textview"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/node_selection_recyclerview"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/node_selection_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="128dp"
android:layout_marginTop="16dp"
android:nestedScrollingEnabled="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_node_button">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/node_name_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Node Name"
android:textStyle="bold"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/node_uri_textview"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/node_uri_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="NODE::"
android:ellipsize="middle"
app:layout_constraintTop_toBottomOf="@id/node_name_textview"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:singleLine="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -76,9 +76,17 @@
<string name="wallet">Wallet</string>
<string name="appearance">Appearance</string>
<string name="network">Network</string>
<string name="add_node">Add Node</string>
<string name="nodes">Nodes</string>
<string name="node_name_hint">My Monero Node</string>
<string name="node_address_hint">127.0.0.1:18081</string>
<string name="wallet_proxy_address_hint">127.0.0.1</string>
<string name="wallet_proxy_port_hint">9050</string>
<string name="invalid_ip">Invalid IP address</string>
<string name="no_history_nget_some_monero_in_here">No history!\nGet some Monero in here!</string>
<string name="node_button_text">Node: %1$s</string>
<string name="connected">Connected</string>
<string name="disconnected">Disconnected</string>
<string name="version_mismatch">Version mismatch</string>
</resources>