You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Wonerujo/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java

605 lines
22 KiB

/*
* Copyright (c) 2018 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;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.levin.scanner.Dispatcher;
import com.m2049r.xmrwallet.data.DefaultNodes;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.data.NodeInfo;
import com.m2049r.xmrwallet.layout.NodeInfoAdapter;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.NodePinger;
import com.m2049r.xmrwallet.util.Notice;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import timber.log.Timber;
public class NodeFragment extends Fragment
implements NodeInfoAdapter.OnInteractionListener, View.OnClickListener {
static private int NODES_TO_FIND = 10;
static private NumberFormat FORMATTER = NumberFormat.getInstance();
private SwipeRefreshLayout pullToRefresh;
private TextView tvPull;
private View fab;
private Set<NodeInfo> nodeList = new HashSet<>();
private NodeInfoAdapter nodesAdapter;
private Listener activityCallback;
public interface Listener {
File getStorageRoot();
void setToolbarButton(int type);
void setSubtitle(String title);
Set<NodeInfo> getFavouriteNodes();
Set<NodeInfo> getOrPopulateFavourites();
void setFavouriteNodes(Collection<NodeInfo> favouriteNodes);
void setNode(NodeInfo node);
}
void filterFavourites() {
for (Iterator<NodeInfo> iter = nodeList.iterator(); iter.hasNext(); ) {
Node node = iter.next();
if (!node.isFavourite()) iter.remove();
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.activityCallback = (Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onPause() {
Timber.d("onPause() %d", nodeList.size());
if (asyncFindNodes != null)
asyncFindNodes.cancel(true);
if (activityCallback != null)
activityCallback.setFavouriteNodes(nodeList);
super.onPause();
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setSubtitle(getString(R.string.label_nodes));
updateRefreshElements();
}
boolean isRefreshing() {
return asyncFindNodes != null;
}
void updateRefreshElements() {
if (isRefreshing()) {
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
fab.setVisibility(View.GONE);
} else {
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
fab.setVisibility(View.VISIBLE);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView");
View view = inflater.inflate(R.layout.fragment_node, container, false);
fab = view.findViewById(R.id.fab);
fab.setOnClickListener(this);
RecyclerView recyclerView = view.findViewById(R.id.list);
nodesAdapter = new NodeInfoAdapter(getActivity(), this);
recyclerView.setAdapter(nodesAdapter);
tvPull = view.findViewById(R.id.tvPull);
pullToRefresh = view.findViewById(R.id.pullToRefresh);
pullToRefresh.setOnRefreshListener(() -> {
if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) {
refresh(AsyncFindNodes.SCAN);
} else {
Toast.makeText(getActivity(), getString(R.string.node_wrong_net), Toast.LENGTH_LONG).show();
pullToRefresh.setRefreshing(false);
}
});
Helper.hideKeyboard(getActivity());
nodeList = new HashSet<>(activityCallback.getFavouriteNodes());
nodesAdapter.setNodes(nodeList);
ViewGroup llNotice = view.findViewById(R.id.llNotice);
Notice.showAll(llNotice, ".*_nodes");
refresh(AsyncFindNodes.PING); // start connection tests
return view;
}
private AsyncFindNodes asyncFindNodes = null;
private boolean refresh(int type) {
if (asyncFindNodes != null) return false; // ignore refresh request as one is ongoing
asyncFindNodes = new AsyncFindNodes();
updateRefreshElements();
asyncFindNodes.execute(type);
return true;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.node_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
// Callbacks from NodeInfoAdapter
@Override
public void onInteraction(final View view, final NodeInfo nodeItem) {
Timber.d("onInteraction");
if (!nodeItem.isFavourite()) {
nodeItem.setFavourite(true);
activityCallback.setFavouriteNodes(nodeList);
}
AsyncTask.execute(() -> {
activityCallback.setNode(nodeItem); // this marks it as selected & saves it as well
nodeItem.setSelecting(false);
try {
Objects.requireNonNull(getActivity()).runOnUiThread(() -> nodesAdapter.allowClick(true));
} catch (NullPointerException ex) {
// it's ok
}
});
}
// open up edit dialog
@Override
public boolean onLongInteraction(final View view, final NodeInfo nodeItem) {
Timber.d("onLongInteraction");
EditDialog diag = createEditDialog(nodeItem);
if (diag != null) {
diag.show();
}
return true;
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.fab) {
EditDialog diag = createEditDialog(null);
if (diag != null) {
diag.show();
}
}
}
private class AsyncFindNodes extends AsyncTask<Integer, NodeInfo, Boolean>
implements NodePinger.Listener {
final static int SCAN = 0;
final static int RESTORE_DEFAULTS = 1;
final static int PING = 2;
@Override
protected void onPreExecute() {
super.onPreExecute();
filterFavourites();
nodesAdapter.setNodes(null);
nodesAdapter.allowClick(false);
tvPull.setText(getString(R.string.node_scanning));
}
@Override
protected Boolean doInBackground(Integer... params) {
if (params[0] == RESTORE_DEFAULTS) { // true = restore defaults
for (DefaultNodes node : DefaultNodes.values()) {
NodeInfo nodeInfo = NodeInfo.fromString(node.getUri());
if (nodeInfo != null) {
nodeInfo.setFavourite(true);
nodeList.add(nodeInfo);
}
}
NodePinger.execute(nodeList, this);
return true;
} else if (params[0] == PING) {
NodePinger.execute(nodeList, this);
return true;
} else if (params[0] == SCAN) {
// otherwise scan the network
Timber.d("scanning");
Set<NodeInfo> seedList = new HashSet<>();
seedList.addAll(nodeList);
nodeList.clear();
Timber.d("seed %d", seedList.size());
Dispatcher d = new Dispatcher(info -> publishProgress(info));
d.seedPeers(seedList);
d.awaitTermination(NODES_TO_FIND);
// we didn't find enough because we didn't ask around enough? ask more!
if ((d.getRpcNodes().size() < NODES_TO_FIND) &&
(d.getPeerCount() < NODES_TO_FIND + seedList.size())) {
// try again
publishProgress((NodeInfo[]) null);
d = new Dispatcher(new Dispatcher.Listener() {
@Override
public void onGet(NodeInfo info) {
publishProgress(info);
}
});
Timber.d("MOREMOREMORE");
// also seed with wownero seed nodes (see p2p/net_node.inl in wownero src)
seedList.add(new NodeInfo(new InetSocketAddress("158.69.60.225", 34567)));
seedList.add(new NodeInfo(new InetSocketAddress("159.65.91.59", 34567)));
seedList.add(new NodeInfo(new InetSocketAddress("164.90.230.176", 34567)));
seedList.add(new NodeInfo(new InetSocketAddress("64.227.81.144", 34567)));
seedList.add(new NodeInfo(new InetSocketAddress("188.166.237.187", 34567)));
seedList.add(new NodeInfo(new InetSocketAddress("51.161.131.176", 34567)));
seedList.add(new NodeInfo(new InetSocketAddress("167.114.196.241", 34567)));
d.seedPeers(seedList);
d.awaitTermination(NODES_TO_FIND);
}
// final (filtered) result
nodeList.addAll(d.getRpcNodes());
return true;
}
return false;
}
@Override
protected void onProgressUpdate(NodeInfo... values) {
Timber.d("onProgressUpdate");
if (!isCancelled())
if (values != null)
nodesAdapter.addNode(values[0]);
else
nodesAdapter.setNodes(null);
}
@Override
protected void onPostExecute(Boolean result) {
Timber.d("done scanning");
complete();
}
@Override
protected void onCancelled(Boolean result) {
Timber.d("cancelled scanning");
complete();
}
private void complete() {
asyncFindNodes = null;
if (!isAdded()) return;
//if (isCancelled()) return;
tvPull.setText(getString(R.string.node_pull_hint));
pullToRefresh.setRefreshing(false);
nodesAdapter.setNodes(nodeList);
nodesAdapter.allowClick(true);
updateRefreshElements();
}
public void publish(NodeInfo nodeInfo) {
publishProgress(nodeInfo);
}
}
@Override
public void onDetach() {
Timber.d("detached");
super.onDetach();
}
private EditDialog editDialog = null; // for preventing opening of multiple dialogs
private EditDialog createEditDialog(final NodeInfo nodeInfo) {
if (editDialog != null) return null; // we are already open
editDialog = new EditDialog(nodeInfo);
return editDialog;
}
class EditDialog {
final NodeInfo nodeInfo;
final NodeInfo nodeBackup;
private boolean applyChanges() {
nodeInfo.clear();
showTestResult();
final String portString = etNodePort.getEditText().getText().toString().trim();
int port;
if (portString.isEmpty()) {
port = Node.getDefaultRpcPort();
} else {
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException ex) {
etNodePort.setError(getString(R.string.node_port_numeric));
return false;
}
}
etNodePort.setError(null);
if ((port <= 0) || (port > 65535)) {
etNodePort.setError(getString(R.string.node_port_range));
return false;
}
final String host = etNodeHost.getEditText().getText().toString().trim();
if (host.isEmpty()) {
etNodeHost.setError(getString(R.string.node_host_empty));
return false;
}
final boolean setHostSuccess = Helper.runWithNetwork(new Helper.Action() {
@Override
public boolean run() {
try {
nodeInfo.setHost(host);
return true;
} catch (UnknownHostException ex) {
etNodeHost.setError(getString(R.string.node_host_unresolved));
return false;
}
}
});
if (!setHostSuccess) {
etNodeHost.setError(getString(R.string.node_host_unresolved));
return false;
}
etNodeHost.setError(null);
nodeInfo.setRpcPort(port);
// setName() may trigger reverse DNS
Helper.runWithNetwork(new Helper.Action() {
@Override
public boolean run() {
nodeInfo.setName(etNodeName.getEditText().getText().toString().trim());
return true;
}
});
nodeInfo.setUsername(etNodeUser.getEditText().getText().toString().trim());
nodeInfo.setPassword(etNodePass.getEditText().getText().toString()); // no trim for pw
return true;
}
private boolean shutdown = false;
private void apply() {
if (applyChanges()) {
closeDialog();
if (nodeBackup == null) { // this is a (FAB) new node
nodeInfo.setFavourite(true);
nodeList.add(nodeInfo);
}
shutdown = true;
new AsyncTestNode().execute();
}
}
private void closeDialog() {
if (editDialog == null) throw new IllegalStateException();
Helper.hideKeyboardAlways(getActivity());
editDialog.dismiss();
editDialog = null;
NodeFragment.this.editDialog = null;
}
private void show() {
editDialog.show();
}
private void test() {
if (applyChanges())
new AsyncTestNode().execute();
}
private void showKeyboard() {
Helper.showKeyboard(editDialog);
}
AlertDialog editDialog = null;
TextInputLayout etNodeName;
TextInputLayout etNodeHost;
TextInputLayout etNodePort;
TextInputLayout etNodeUser;
TextInputLayout etNodePass;
TextView tvResult;
void showTestResult() {
if (nodeInfo.isSuccessful()) {
tvResult.setText(getString(R.string.node_result,
FORMATTER.format(nodeInfo.getHeight()), nodeInfo.getMajorVersion(),
nodeInfo.getResponseTime(), nodeInfo.getHostAddress()));
} else {
tvResult.setText(NodeInfoAdapter.getResponseErrorText(getActivity(), nodeInfo.getResponseCode()));
}
}
EditDialog(final NodeInfo nodeInfo) {
AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(getActivity());
LayoutInflater li = LayoutInflater.from(alertDialogBuilder.getContext());
View promptsView = li.inflate(R.layout.prompt_editnode, null);
alertDialogBuilder.setView(promptsView);
etNodeName = promptsView.findViewById(R.id.etNodeName);
etNodeHost = promptsView.findViewById(R.id.etNodeHost);
etNodePort = promptsView.findViewById(R.id.etNodePort);
etNodeUser = promptsView.findViewById(R.id.etNodeUser);
etNodePass = promptsView.findViewById(R.id.etNodePass);
tvResult = promptsView.findViewById(R.id.tvResult);
if (nodeInfo != null) {
this.nodeInfo = nodeInfo;
nodeBackup = new NodeInfo(nodeInfo);
etNodeName.getEditText().setText(nodeInfo.getName());
etNodeHost.getEditText().setText(nodeInfo.getHost());
etNodePort.getEditText().setText(Integer.toString(nodeInfo.getRpcPort()));
etNodeUser.getEditText().setText(nodeInfo.getUsername());
etNodePass.getEditText().setText(nodeInfo.getPassword());
showTestResult();
} else {
this.nodeInfo = new NodeInfo();
nodeBackup = null;
}
// set dialog message
alertDialogBuilder
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok), null)
.setNeutralButton(getString(R.string.label_test), null)
.setNegativeButton(getString(R.string.label_cancel),
(dialog, id) -> {
closeDialog();
nodesAdapter.setNodes(); // to refresh test results
});
editDialog = alertDialogBuilder.create();
// these need to be here, since we don't always close the dialog
editDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(final DialogInterface dialog) {
Button testButton = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEUTRAL);
testButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
test();
}
});
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
apply();
}
});
}
});
if (Helper.preventScreenshot()) {
editDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
etNodePass.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
editDialog.getButton(DialogInterface.BUTTON_NEUTRAL).requestFocus();
test();
return true;
}
return false;
}
});
}
private class AsyncTestNode extends AsyncTask<Void, Void, Boolean> {
@Override
protected void onPreExecute() {
super.onPreExecute();
tvResult.setText(getString(R.string.node_testing, nodeInfo.getHostAddress()));
}
@Override
protected Boolean doInBackground(Void... params) {
nodeInfo.testRpcService();
return true;
}
@Override
protected void onPostExecute(Boolean result) {
if (editDialog != null) {
showTestResult();
}
if (shutdown) {
if (nodeBackup == null) {
nodesAdapter.addNode(nodeInfo);
} else {
nodesAdapter.setNodes();
}
}
}
}
}
void restoreDefaultNodes() {
if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) {
if (!refresh(AsyncFindNodes.RESTORE_DEFAULTS)) {
Toast.makeText(getActivity(), getString(R.string.toast_default_nodes), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getActivity(), getString(R.string.node_wrong_net), Toast.LENGTH_LONG).show();
}
}
}