parent
2dad55e498
commit
973472e0ef
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.levin.data;
|
||||
|
||||
import com.m2049r.levin.util.HexHelper;
|
||||
import com.m2049r.levin.util.LevinReader;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Bucket {
|
||||
|
||||
// constants copied from monero p2p & epee
|
||||
|
||||
public final static int P2P_COMMANDS_POOL_BASE = 1000;
|
||||
public final static int COMMAND_HANDSHAKE_ID = P2P_COMMANDS_POOL_BASE + 1;
|
||||
public final static int COMMAND_TIMED_SYNC_ID = P2P_COMMANDS_POOL_BASE + 2;
|
||||
public final static int COMMAND_PING_ID = P2P_COMMANDS_POOL_BASE + 3;
|
||||
public final static int COMMAND_REQUEST_STAT_INFO_ID = P2P_COMMANDS_POOL_BASE + 4;
|
||||
public final static int COMMAND_REQUEST_NETWORK_STATE_ID = P2P_COMMANDS_POOL_BASE + 5;
|
||||
public final static int COMMAND_REQUEST_PEER_ID_ID = P2P_COMMANDS_POOL_BASE + 6;
|
||||
public final static int COMMAND_REQUEST_SUPPORT_FLAGS_ID = P2P_COMMANDS_POOL_BASE + 7;
|
||||
|
||||
public final static long LEVIN_SIGNATURE = 0x0101010101012101L; // Bender's nightmare
|
||||
|
||||
public final static long LEVIN_DEFAULT_MAX_PACKET_SIZE = 100000000; // 100MB by default
|
||||
|
||||
public final static int LEVIN_PACKET_REQUEST = 0x00000001;
|
||||
public final static int LEVIN_PACKET_RESPONSE = 0x00000002;
|
||||
|
||||
public final static int LEVIN_PROTOCOL_VER_0 = 0;
|
||||
public final static int LEVIN_PROTOCOL_VER_1 = 1;
|
||||
|
||||
public final static int LEVIN_OK = 0;
|
||||
public final static int LEVIN_ERROR_CONNECTION = -1;
|
||||
public final static int LEVIN_ERROR_CONNECTION_NOT_FOUND = -2;
|
||||
public final static int LEVIN_ERROR_CONNECTION_DESTROYED = -3;
|
||||
public final static int LEVIN_ERROR_CONNECTION_TIMEDOUT = -4;
|
||||
public final static int LEVIN_ERROR_CONNECTION_NO_DUPLEX_PROTOCOL = -5;
|
||||
public final static int LEVIN_ERROR_CONNECTION_HANDLER_NOT_DEFINED = -6;
|
||||
public final static int LEVIN_ERROR_FORMAT = -7;
|
||||
|
||||
public final static int P2P_SUPPORT_FLAG_FLUFFY_BLOCKS = 0x01;
|
||||
public final static int P2P_SUPPORT_FLAGS = P2P_SUPPORT_FLAG_FLUFFY_BLOCKS;
|
||||
|
||||
final private long signature;
|
||||
final private long cb;
|
||||
final public boolean haveToReturnData;
|
||||
final public int command;
|
||||
final public int returnCode;
|
||||
final private int flags;
|
||||
final private int protcolVersion;
|
||||
final byte[] payload;
|
||||
|
||||
final public Section payloadSection;
|
||||
|
||||
// create a request
|
||||
public Bucket(int command, byte[] payload) throws IOException {
|
||||
this.signature = LEVIN_SIGNATURE;
|
||||
this.cb = payload.length;
|
||||
this.haveToReturnData = true;
|
||||
this.command = command;
|
||||
this.returnCode = 0;
|
||||
this.flags = LEVIN_PACKET_REQUEST;
|
||||
this.protcolVersion = LEVIN_PROTOCOL_VER_1;
|
||||
this.payload = payload;
|
||||
payloadSection = LevinReader.readPayload(payload);
|
||||
}
|
||||
|
||||
// create a response
|
||||
public Bucket(int command, byte[] payload, int rc) throws IOException {
|
||||
this.signature = LEVIN_SIGNATURE;
|
||||
this.cb = payload.length;
|
||||
this.haveToReturnData = false;
|
||||
this.command = command;
|
||||
this.returnCode = rc;
|
||||
this.flags = LEVIN_PACKET_RESPONSE;
|
||||
this.protcolVersion = LEVIN_PROTOCOL_VER_1;
|
||||
this.payload = payload;
|
||||
payloadSection = LevinReader.readPayload(payload);
|
||||
}
|
||||
|
||||
public Bucket(DataInput in) throws IOException {
|
||||
signature = in.readLong();
|
||||
cb = in.readLong();
|
||||
haveToReturnData = in.readBoolean();
|
||||
command = in.readInt();
|
||||
returnCode = in.readInt();
|
||||
flags = in.readInt();
|
||||
protcolVersion = in.readInt();
|
||||
|
||||
if (signature == Bucket.LEVIN_SIGNATURE) {
|
||||
if (cb > Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException();
|
||||
payload = new byte[(int) cb];
|
||||
in.readFully(payload);
|
||||
} else
|
||||
throw new IllegalStateException();
|
||||
payloadSection = LevinReader.readPayload(payload);
|
||||
}
|
||||
|
||||
public Section getPayloadSection() {
|
||||
return payloadSection;
|
||||
}
|
||||
|
||||
public void send(DataOutput out) throws IOException {
|
||||
out.writeLong(signature);
|
||||
out.writeLong(cb);
|
||||
out.writeBoolean(haveToReturnData);
|
||||
out.writeInt(command);
|
||||
out.writeInt(returnCode);
|
||||
out.writeInt(flags);
|
||||
out.writeInt(protcolVersion);
|
||||
out.write(payload);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("sig: ").append(signature).append("\n");
|
||||
sb.append("cb: ").append(cb).append("\n");
|
||||
sb.append("call: ").append(haveToReturnData).append("\n");
|
||||
sb.append("cmd: ").append(command).append("\n");
|
||||
sb.append("rc: ").append(returnCode).append("\n");
|
||||
sb.append("flags:").append(flags).append("\n");
|
||||
sb.append("proto:").append(protcolVersion).append("\n");
|
||||
sb.append(HexHelper.bytesToHex(payload)).append("\n");
|
||||
sb.append(payloadSection.toString());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.levin.data;
|
||||
|
||||
import com.m2049r.levin.util.HexHelper;
|
||||
import com.m2049r.levin.util.LevinReader;
|
||||
import com.m2049r.levin.util.LevinWriter;
|
||||
import com.m2049r.levin.util.LittleEndianDataOutputStream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class Section {
|
||||
|
||||
// constants copied from monero p2p & epee
|
||||
|
||||
static final public int PORTABLE_STORAGE_SIGNATUREA = 0x01011101;
|
||||
static final public int PORTABLE_STORAGE_SIGNATUREB = 0x01020101;
|
||||
|
||||
static final public byte PORTABLE_STORAGE_FORMAT_VER = 1;
|
||||
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_MASK = 0x03;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_BYTE = 0;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_WORD = 1;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_DWORD = 2;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_INT64 = 3;
|
||||
|
||||
static final long MAX_STRING_LEN_POSSIBLE = 2000000000; // do not let string be so big
|
||||
|
||||
// data types
|
||||
static final public byte SERIALIZE_TYPE_INT64 = 1;
|
||||
static final public byte SERIALIZE_TYPE_INT32 = 2;
|
||||
static final public byte SERIALIZE_TYPE_INT16 = 3;
|
||||
static final public byte SERIALIZE_TYPE_INT8 = 4;
|
||||
static final public byte SERIALIZE_TYPE_UINT64 = 5;
|
||||
static final public byte SERIALIZE_TYPE_UINT32 = 6;
|
||||
static final public byte SERIALIZE_TYPE_UINT16 = 7;
|
||||
static final public byte SERIALIZE_TYPE_UINT8 = 8;
|
||||
static final public byte SERIALIZE_TYPE_DUOBLE = 9;
|
||||
static final public byte SERIALIZE_TYPE_STRING = 10;
|
||||
static final public byte SERIALIZE_TYPE_BOOL = 11;
|
||||
static final public byte SERIALIZE_TYPE_OBJECT = 12;
|
||||
static final public byte SERIALIZE_TYPE_ARRAY = 13;
|
||||
|
||||
static final public byte SERIALIZE_FLAG_ARRAY = (byte) 0x80;
|
||||
|
||||
private final Map<String, Object> entries = new HashMap<String, Object>();
|
||||
|
||||
public void add(String key, Object entry) {
|
||||
entries.put(key, entry);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
public Set<Map.Entry<String, Object>> entrySet() {
|
||||
return entries.entrySet();
|
||||
}
|
||||
|
||||
public Object get(String key) {
|
||||
return entries.get(key);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
for (Map.Entry<String, Object> entry : entries.entrySet()) {
|
||||
sb.append(entry.getKey()).append("=");
|
||||
final Object value = entry.getValue();
|
||||
if (value instanceof List) {
|
||||
@SuppressWarnings("unchecked") final List<Object> list = (List<Object>) value;
|
||||
for (Object listEntry : list) {
|
||||
sb.append(listEntry.toString()).append("\n");
|
||||
}
|
||||
} else if (value instanceof String) {
|
||||
sb.append("(").append(value).append(")\n");
|
||||
} else if (value instanceof byte[]) {
|
||||
sb.append(HexHelper.bytesToHex((byte[]) value)).append("\n");
|
||||
} else {
|
||||
sb.append(value.toString()).append("\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static public Section fromByteArray(byte[] buffer) {
|
||||
try {
|
||||
return LevinReader.readPayload(buffer);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] asByteArray() {
|
||||
try {
|
||||
ByteArrayOutputStream bas = new ByteArrayOutputStream();
|
||||
DataOutput out = new LittleEndianDataOutputStream(bas);
|
||||
LevinWriter writer = new LevinWriter(out);
|
||||
writer.writePayload(this);
|
||||
return bas.toByteArray();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.levin.scanner;
|
||||
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Dispatcher implements PeerRetriever.OnGetPeers {
|
||||
static final public int NUM_THREADS = 50;
|
||||
static final public int MAX_PEERS = 1000;
|
||||
static final public long MAX_TIME = 30000000000L; //30 seconds
|
||||
|
||||
private int peerCount = 0;
|
||||
final private Set<NodeInfo> knownNodes = new HashSet<>(); // set of nodes to test
|
||||
final private Set<NodeInfo> rpcNodes = new HashSet<>(); // set of RPC nodes we like
|
||||
final private ExecutorService exeService = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
|
||||
public interface Listener {
|
||||
void onGet(NodeInfo nodeInfo);
|
||||
}
|
||||
|
||||
private Listener listener;
|
||||
|
||||
public Dispatcher(Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public Set<NodeInfo> getRpcNodes() {
|
||||
return rpcNodes;
|
||||
}
|
||||
|
||||
public int getPeerCount() {
|
||||
return peerCount;
|
||||
}
|
||||
|
||||
public boolean getMorePeers() {
|
||||
return peerCount < MAX_PEERS;
|
||||
}
|
||||
|
||||
public void awaitTermination(int nodesToFind) {
|
||||
try {
|
||||
final long t = System.nanoTime();
|
||||
while (!jobs.isEmpty()) {
|
||||
try {
|
||||
Timber.d("Remaining jobs %d", jobs.size());
|
||||
final PeerRetriever retrievedPeer = jobs.poll().get();
|
||||
if (retrievedPeer.isGood() && getMorePeers())
|
||||
retrievePeers(retrievedPeer);
|
||||
final NodeInfo nodeInfo = retrievedPeer.getNodeInfo();
|
||||
Timber.d("Retrieved %s", nodeInfo);
|
||||
if ((nodeInfo.isValid() || nodeInfo.isFavourite())) {
|
||||
nodeInfo.setName();
|
||||
rpcNodes.add(nodeInfo);
|
||||
Timber.d("RPC: %s", nodeInfo);
|
||||
// the following is not totally correct but it works (otherwise we need to
|
||||
// load much more before filtering - but we don't have time
|
||||
if (listener != null) listener.onGet(nodeInfo);
|
||||
if (rpcNodes.size() >= nodesToFind) {
|
||||
Timber.d("are we done here?");
|
||||
filterRpcNodes();
|
||||
if (rpcNodes.size() >= nodesToFind) {
|
||||
Timber.d("we're done here");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.nanoTime() - t > MAX_TIME) break; // watchdog
|
||||
} catch (ExecutionException ex) {
|
||||
Timber.d(ex); // tell us about it and continue
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Timber.d(ex);
|
||||
} finally {
|
||||
Timber.d("Shutting down!");
|
||||
exeService.shutdownNow();
|
||||
try {
|
||||
exeService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
|
||||
} catch (InterruptedException ex) {
|
||||
Timber.d(ex);
|
||||
}
|
||||
}
|
||||
filterRpcNodes();
|
||||
}
|
||||
|
||||
static final public int HEIGHT_WINDOW = 1;
|
||||
|
||||
private boolean testHeight(long height, long consensus) {
|
||||
return (height >= (consensus - HEIGHT_WINDOW))
|
||||
&& (height <= (consensus + HEIGHT_WINDOW));
|
||||
}
|
||||
|
||||
private long calcConsensusHeight() {
|
||||
Timber.d("Calc Consensus height from %d nodes", rpcNodes.size());
|
||||
final Map<Long, Integer> nodeHeights = new TreeMap<Long, Integer>();
|
||||
for (NodeInfo info : rpcNodes) {
|
||||
if (!info.isValid()) continue;
|
||||
Integer h = nodeHeights.get(info.getHeight());
|
||||
if (h == null)
|
||||
h = 0;
|
||||
nodeHeights.put(info.getHeight(), h + 1);
|
||||
}
|
||||
long consensusHeight = 0;
|
||||
long consensusCount = 0;
|
||||
for (Map.Entry<Long, Integer> entry : nodeHeights.entrySet()) {
|
||||
final long entryHeight = entry.getKey();
|
||||
int count = 0;
|
||||
for (long i = entryHeight - HEIGHT_WINDOW; i <= entryHeight + HEIGHT_WINDOW; i++) {
|
||||
Integer v = nodeHeights.get(i);
|
||||
if (v == null)
|
||||
v = 0;
|
||||
count += v;
|
||||
}
|
||||
if (count >= consensusCount) {
|
||||
consensusCount = count;
|
||||
consensusHeight = entryHeight;
|
||||
}
|
||||
Timber.d("%d - %d/%d", entryHeight, count, entry.getValue());
|
||||
}
|
||||
return consensusHeight;
|
||||
}
|
||||
|
||||
private void filterRpcNodes() {
|
||||
long consensus = calcConsensusHeight();
|
||||
Timber.d("Consensus Height = %d for %d nodes", consensus, rpcNodes.size());
|
||||
for (Iterator<NodeInfo> iter = rpcNodes.iterator(); iter.hasNext(); ) {
|
||||
NodeInfo info = iter.next();
|
||||
// don't remove favourites
|
||||
if (!info.isFavourite()) {
|
||||
if (!testHeight(info.getHeight(), consensus)) {
|
||||
iter.remove();
|
||||
Timber.d("Removed %s", info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: does this NEED to be a ConcurrentLinkedDeque?
|
||||
private ConcurrentLinkedDeque<Future<PeerRetriever>> jobs = new ConcurrentLinkedDeque<>();
|
||||
|
||||
private void retrievePeer(NodeInfo nodeInfo) {
|
||||
if (knownNodes.add(nodeInfo)) {
|
||||
Timber.d("\t%d:%s", knownNodes.size(), nodeInfo);
|
||||
jobs.add(exeService.submit(new PeerRetriever(nodeInfo, this)));
|
||||
peerCount++; // jobs.size() does not perform well
|
||||
}
|
||||
}
|
||||
|
||||
private void retrievePeers(PeerRetriever peer) {
|
||||
for (InetSocketAddress socketAddress : peer.getPeers()) {
|
||||
if (getMorePeers())
|
||||
retrievePeer(new NodeInfo(socketAddress));
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void seedPeers(Collection<NodeInfo> seedNodes) {
|
||||
for (NodeInfo node : seedNodes) {
|
||||
node.clear();
|
||||
if (node.isFavourite()) {
|
||||
rpcNodes.add(node);
|
||||
if (listener != null) listener.onGet(node);
|
||||
}
|
||||
retrievePeer(node);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.levin.scanner;
|
||||
|
||||
import com.m2049r.levin.data.Bucket;
|
||||
import com.m2049r.levin.data.Section;
|
||||
import com.m2049r.levin.util.HexHelper;
|
||||
import com.m2049r.levin.util.LittleEndianDataInputStream;
|
||||
import com.m2049r.levin.util.LittleEndianDataOutputStream;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class PeerRetriever implements Callable<PeerRetriever> {
|
||||
static final public int CONNECT_TIMEOUT = 500; //ms
|
||||
static final public int SOCKET_TIMEOUT = 500; //ms
|
||||
static final public long PEER_ID = new Random().nextLong();
|
||||
static final private byte[] HANDSHAKE = handshakeRequest().asByteArray();
|
||||
static final private byte[] FLAGS_RESP = flagsResponse().asByteArray();
|
||||
|
||||
final private List<InetSocketAddress> peers = new ArrayList<>();
|
||||
|
||||
private NodeInfo nodeInfo;
|
||||
private OnGetPeers onGetPeersCallback;
|
||||
|
||||
public interface OnGetPeers {
|
||||
boolean getMorePeers();
|
||||
}
|
||||
|
||||
public PeerRetriever(NodeInfo nodeInfo, OnGetPeers onGetPeers) {
|
||||
this.nodeInfo = nodeInfo;
|
||||
this.onGetPeersCallback = onGetPeers;
|
||||
}
|
||||
|
||||
public NodeInfo getNodeInfo() {
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
public boolean isGood() {
|
||||
return !peers.isEmpty();
|
||||
}
|
||||
|
||||
public List<InetSocketAddress> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
public PeerRetriever call() {
|
||||
if (isGood()) // we have already been called?
|
||||
throw new IllegalStateException();
|
||||
// first check for an rpc service
|
||||
nodeInfo.findRpcService();
|
||||
if (onGetPeersCallback.getMorePeers())
|
||||
try {
|
||||
Timber.d("%s CONN", nodeInfo.getLevinSocketAddress());
|
||||
if (!connect())
|
||||
return this;
|
||||
Bucket handshakeBucket = new Bucket(Bucket.COMMAND_HANDSHAKE_ID, HANDSHAKE);
|
||||
handshakeBucket.send(getDataOutput());
|
||||
|
||||
while (true) {// wait for response (which may never come)
|
||||
Bucket recv = new Bucket(getDataInput()); // times out after SOCKET_TIMEOUT
|
||||
if ((recv.command == Bucket.COMMAND_HANDSHAKE_ID)
|
||||
&& (!recv.haveToReturnData)) {
|
||||
readAddressList(recv.payloadSection);
|
||||
return this;
|
||||
} else if ((recv.command == Bucket.COMMAND_REQUEST_SUPPORT_FLAGS_ID)
|
||||
&& (recv.haveToReturnData)) {
|
||||
Bucket flagsBucket = new Bucket(Bucket.COMMAND_REQUEST_SUPPORT_FLAGS_ID, FLAGS_RESP, 1);
|
||||
flagsBucket.send(getDataOutput());
|
||||
} else {// and ignore others
|
||||
Timber.d("Ignored LEVIN COMMAND %d", recv.command);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
} finally {
|
||||
disconnect(); // we have what we want - byebye
|
||||
Timber.d("%s DISCONN", nodeInfo.getLevinSocketAddress());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void readAddressList(Section section) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Section> peerList = (List<Section>) section.get("local_peerlist_new");
|
||||
if (peerList != null) {
|
||||
for (Section peer : peerList) {
|
||||
Section adr = (Section) peer.get("adr");
|
||||
Byte type = (Byte) adr.get("type");
|
||||
if ((type == null) || (type != 1))
|
||||
continue;
|
||||
Section addr = (Section) adr.get("addr");
|
||||
if (addr == null)
|
||||
continue;
|
||||
Integer ip = (Integer) addr.get("m_ip");
|
||||
if (ip == null)
|
||||
continue;
|
||||
Short sport = (Short) addr.get("m_port");
|
||||
if (sport == null)
|
||||
continue;
|
||||
int port = sport;
|
||||
if (port < 0) // port is unsigned
|
||||
port = port + 0x10000;
|
||||
InetAddress inet = HexHelper.toInetAddress(ip);
|
||||
// make sure this is an address we want to talk to (i.e. a remote address)
|
||||
if (!inet.isSiteLocalAddress() && !inet.isAnyLocalAddress()
|
||||
&& !inet.isLoopbackAddress()
|
||||
&& !inet.isMulticastAddress()
|
||||
&& !inet.isLinkLocalAddress()) {
|
||||
peers.add(new InetSocketAddress(inet, port));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Socket socket = null;
|
||||
|
||||
private boolean connect() {
|
||||
if (socket != null) throw new IllegalStateException();
|
||||
try {
|
||||
socket = new Socket();
|
||||
socket.connect(nodeInfo.getLevinSocketAddress(), CONNECT_TIMEOUT);
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT);
|
||||
} catch (IOException ex) {
|
||||
//Timber.d(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
return socket.isConnected();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
try {
|
||||
dataInput = null;
|
||||
dataOutput = null;
|
||||
if ((socket != null) && (!socket.isClosed())) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Timber.d(ex);
|
||||
} finally {
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private DataOutput dataOutput = null;
|
||||
|
||||
private DataOutput getDataOutput() throws IOException {
|
||||
if (dataOutput == null)
|
||||
synchronized (this) {
|
||||
if (dataOutput == null)
|
||||
dataOutput = new LittleEndianDataOutputStream(
|
||||
socket.getOutputStream());
|
||||
}
|
||||
return dataOutput;
|
||||
}
|
||||
|
||||
private DataInput dataInput = null;
|
||||
|
||||
private DataInput getDataInput() throws IOException {
|
||||
if (dataInput == null)
|
||||
synchronized (this) {
|
||||
if (dataInput == null)
|
||||
dataInput = new LittleEndianDataInputStream(
|
||||
socket.getInputStream());
|
||||
}
|
||||
return dataInput;
|
||||
}
|
||||
|
||||
static private Section handshakeRequest() {
|
||||
Section section = new Section(); // root object
|
||||
|
||||
Section nodeData = new Section();
|
||||
nodeData.add("local_time", (new Date()).getTime());
|
||||
nodeData.add("my_port", 0);
|
||||
byte[] networkId = Helper.hexToBytes("1230f171610441611731008216a1a110"); // mainnet
|
||||
nodeData.add("network_id", networkId);
|
||||
nodeData.add("peer_id", PEER_ID);
|
||||
section.add("node_data", nodeData);
|
||||
|
||||
Section payloadData = new Section();
|
||||
payloadData.add("cumulative_difficulty", 1L);
|
||||
payloadData.add("current_height", 1L);
|
||||
byte[] genesisHash =
|
||||
Helper.hexToBytes("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3");
|
||||
payloadData.add("top_id", genesisHash);
|
||||
payloadData.add("top_version", (byte) 1);
|
||||
section.add("payload_data", payloadData);
|
||||
return section;
|
||||
}
|
||||
|
||||
static private Section flagsResponse() {
|
||||
Section section = new Section(); // root object
|
||||
section.add("support_flags", Bucket.P2P_SUPPORT_FLAGS);
|
||||
return section;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
public class HexHelper {
|
||||
|
||||
static public String bytesToHex(byte[] data) {
|
||||
if ((data != null) && (data.length > 0))
|
||||
return String.format("%0" + (data.length * 2) + "X",
|
||||
new BigInteger(1, data));
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
static public InetAddress toInetAddress(int ip) {
|
||||
try {
|
||||
String ipAddress = String.format("%d.%d.%d.%d", (ip & 0xff),
|
||||
(ip >> 8 & 0xff), (ip >> 16 & 0xff), (ip >> 24 & 0xff));
|
||||
return InetAddress.getByName(ipAddress);
|
||||
} catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import com.m2049r.levin.data.Section;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
// Full Levin reader as seen on epee
|
||||
|
||||
public class LevinReader {
|
||||
private DataInput in;
|
||||
|
||||
private LevinReader(byte[] buffer) {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
|
||||
in = new LittleEndianDataInputStream(bis);
|
||||
}
|
||||
|
||||
static public Section readPayload(byte[] payload) throws IOException {
|
||||
LevinReader r = new LevinReader(payload);
|
||||
return r.readPayload();
|
||||
}
|
||||
|
||||
private Section readPayload() throws IOException {
|
||||
if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREA)
|
||||
throw new IllegalStateException();
|
||||
if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREB)
|
||||
throw new IllegalStateException();
|
||||
if (in.readByte() != Section.PORTABLE_STORAGE_FORMAT_VER)
|
||||
throw new IllegalStateException();
|
||||
return readSection();
|
||||
}
|
||||
|
||||
private Section readSection() throws IOException {
|
||||
Section section = new Section();
|
||||
long count = readVarint();
|
||||
while (count-- > 0) {
|
||||
// read section name string
|
||||
String sectionName = readSectionName();
|
||||
section.add(sectionName, loadStorageEntry());
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
private Object loadStorageArrayEntry(int type) throws IOException {
|
||||
type &= ~Section.SERIALIZE_FLAG_ARRAY;
|
||||
return readArrayEntry(type);
|
||||
}
|
||||
|
||||
private List<Object> readArrayEntry(int type) throws IOException {
|
||||
List<Object> list = new ArrayList<Object>();
|
||||
long size = readVarint();
|
||||
while (size-- > 0)
|
||||
list.add(read(type));
|
||||
return list;
|
||||
}
|
||||
|
||||
private Object read(int type) throws IOException {
|
||||
switch (type) {
|
||||
case Section.SERIALIZE_TYPE_UINT64:
|
||||
case Section.SERIALIZE_TYPE_INT64:
|
||||
return in.readLong();
|
||||
case Section.SERIALIZE_TYPE_UINT32:
|
||||
case Section.SERIALIZE_TYPE_INT32:
|
||||
return in.readInt();
|
||||
case Section.SERIALIZE_TYPE_UINT16:
|
||||
case Section.SERIALIZE_TYPE_INT16:
|
||||
return in.readShort();
|
||||
case Section.SERIALIZE_TYPE_UINT8:
|
||||
case Section.SERIALIZE_TYPE_INT8:
|
||||
return in.readByte();
|
||||
case Section.SERIALIZE_TYPE_OBJECT:
|
||||
return readSection();
|
||||
case Section.SERIALIZE_TYPE_STRING:
|
||||
return readByteArray();
|
||||
default:
|
||||
throw new IllegalArgumentException("type " + type
|
||||
+ " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private Object loadStorageEntry() throws IOException {
|
||||
int type = in.readUnsignedByte();
|
||||
if ((type & Section.SERIALIZE_FLAG_ARRAY) != 0)
|
||||
return loadStorageArrayEntry(type);
|
||||
if (type == Section.SERIALIZE_TYPE_ARRAY)
|
||||
return readStorageEntryArrayEntry();
|
||||
else
|
||||
return readStorageEntry(type);
|
||||
}
|
||||
|
||||
private Object readStorageEntry(int type) throws IOException {
|
||||
return read(type);
|
||||
}
|
||||
|
||||
private Object readStorageEntryArrayEntry() throws IOException {
|
||||
int type = in.readUnsignedByte();
|
||||
if ((type & Section.SERIALIZE_FLAG_ARRAY) != 0)
|
||||
throw new IllegalStateException("wrong type sequences");
|
||||
return loadStorageArrayEntry(type);
|
||||
}
|
||||
|
||||
private String readSectionName() throws IOException {
|
||||
int nameLen = in.readUnsignedByte();
|
||||
return readString(nameLen);
|
||||
}
|
||||
|
||||
private byte[] read(long count) throws IOException {
|
||||
if (count > Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException();
|
||||
int len = (int) count;
|
||||
final byte buffer[] = new byte[len];
|
||||
in.readFully(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private String readString(long count) throws IOException {
|
||||
return new String(read(count), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private byte[] readByteArray(long count) throws IOException {
|
||||
return read(count);
|
||||
}
|
||||
|
||||
private byte[] readByteArray() throws IOException {
|
||||
long len = readVarint();
|
||||
return readByteArray(len);
|
||||
}
|
||||
|
||||
private long readVarint() throws IOException {
|
||||
long v = 0;
|
||||
int b = in.readUnsignedByte();
|
||||
int sizeMask = b & Section.PORTABLE_RAW_SIZE_MARK_MASK;
|
||||
switch (sizeMask) {
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_BYTE:
|
||||
v = b >>> 2;
|
||||
break;
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_WORD:
|
||||
v = readRest(b, 1) >>> 2;
|
||||
break;
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_DWORD:
|
||||
v = readRest(b, 3) >>> 2;
|
||||
break;
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_INT64:
|
||||
v = readRest(b, 7) >>> 2;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// this should be in LittleEndianDataInputStream because it has little
|
||||
// endian logic
|
||||
private long readRest(int firstByte, int bytes) throws IOException {
|
||||
long result = firstByte;
|
||||
for (int i = 0; i < bytes; i++) {
|
||||
result = result + (in.readUnsignedByte() << 8);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import com.m2049r.levin.data.Section;
|
||||
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
// a simplified Levin Writer WITHOUT support for arrays
|
||||
|
||||
public class LevinWriter {
|
||||
private DataOutput out;
|
||||
|
||||
public LevinWriter(DataOutput out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
public void writePayload(Section section) throws IOException {
|
||||
out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREA);
|
||||
out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREB);
|
||||
out.writeByte(Section.PORTABLE_STORAGE_FORMAT_VER);
|
||||
putSection(section);
|
||||
}
|
||||
|
||||
private void writeSection(Section section) throws IOException {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_OBJECT);
|
||||
putSection(section);
|
||||
}
|
||||
|
||||
private void putSection(Section section) throws IOException {
|
||||
writeVarint(section.size());
|
||||
for (Map.Entry<String, Object> kv : section.entrySet()) {
|
||||
byte[] key = kv.getKey().getBytes(StandardCharsets.US_ASCII);
|
||||
out.writeByte(key.length);
|
||||
out.write(key);
|
||||
write(kv.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeVarint(long i) throws IOException {
|
||||
if (i <= 63) {
|
||||
out.writeByte(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_BYTE);
|
||||
} else if (i <= 16383) {
|
||||
out.writeShort(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_WORD);
|
||||
} else if (i <= 1073741823) {
|
||||
out.writeInt(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_DWORD);
|
||||
} else {
|
||||
if (i > 4611686018427387903L)
|
||||
throw new IllegalArgumentException();
|
||||
out.writeLong((i << 2) | Section.PORTABLE_RAW_SIZE_MARK_INT64);
|
||||
}
|
||||
}
|
||||
|
||||
private void write(Object object) throws IOException {
|
||||
if (object instanceof byte[]) {
|
||||
byte[] value = (byte[]) object;
|
||||
out.writeByte(Section.SERIALIZE_TYPE_STRING);
|
||||
writeVarint(value.length);
|
||||
out.write(value);
|
||||
} else if (object instanceof String) {
|
||||
byte[] value = ((String) object)
|
||||
.getBytes(StandardCharsets.US_ASCII);
|
||||
out.writeByte(Section.SERIALIZE_TYPE_STRING);
|
||||
writeVarint(value.length);
|
||||
out.write(value);
|
||||
} else if (object instanceof Integer) {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_UINT32);
|
||||
out.writeInt((int) object);
|
||||
} else if (object instanceof Long) {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_UINT64);
|
||||
out.writeLong((long) object);
|
||||
} else if (object instanceof Byte) {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_UINT8);
|
||||
out.writeByte((byte) object);
|
||||
} else if (object instanceof Section) {
|
||||
writeSection((Section) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,564 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UTFDataFormatException;
|
||||
|
||||
/**
|
||||
* A little endian java.io.DataInputStream (without readLine())
|
||||
*/
|
||||
|
||||
public class LittleEndianDataInputStream extends FilterInputStream implements
|
||||
DataInput {
|
||||
|
||||
/**
|
||||
* Creates a DataInputStream that uses the specified underlying InputStream.
|
||||
*
|
||||
* @param in the specified input stream
|
||||
*/
|
||||
public LittleEndianDataInputStream(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public final String readLine() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads some number of bytes from the contained input stream and stores
|
||||
* them into the buffer array <code>b</code>. The number of bytes actually
|
||||
* read is returned as an integer. This method blocks until input data is
|
||||
* available, end of file is detected, or an exception is thrown.
|
||||
*
|
||||
* <p>
|
||||
* If <code>b</code> is null, a <code>NullPointerException</code> is thrown.
|
||||
* If the length of <code>b</code> is zero, then no bytes are read and
|
||||
* <code>0</code> is returned; otherwise, there is an attempt to read at
|
||||
* least one byte. If no byte is available because the stream is at end of
|
||||
* file, the value <code>-1</code> is returned; otherwise, at least one byte
|
||||
* is read and stored into <code>b</code>.
|
||||
*
|
||||
* <p>
|
||||
* The first byte read is stored into element <code>b[0]</code>, the next
|
||||
* one into <code>b[1]</code>, and so on. The number of bytes read is, at
|
||||
* most, equal to the length of <code>b</code>. Let <code>k</code> be the
|
||||
* number of bytes actually read; these bytes will be stored in elements
|
||||
* <code>b[0]</code> through <code>b[k-1]</code>, leaving elements
|
||||
* <code>b[k]</code> through <code>b[b.length-1]</code> unaffected.
|
||||
*
|
||||
* <p>
|
||||
* The <code>read(b)</code> method has the same effect as: <blockquote>
|
||||
*
|
||||
* <pre>
|
||||
* read(b, 0, b.length)
|
||||
* </pre>
|
||||
*
|
||||
* </blockquote>
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> if there is no more data because the end of the
|
||||
* stream has been reached.
|
||||
* @throws IOException if the first byte cannot be read for any reason other than
|
||||
* end of file, the stream has been closed and the underlying
|
||||
* input stream does not support reading after close, or
|
||||
* another I/O error occurs.
|
||||
* @see FilterInputStream#in
|
||||
* @see InputStream#read(byte[], int, int)
|
||||
*/
|
||||
public final int read(byte b[]) throws IOException {
|
||||
return in.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to <code>len</code> bytes of data from the contained input
|
||||
* stream into an array of bytes. An attempt is made to read as many as
|
||||
* <code>len</code> bytes, but a smaller number may be read, possibly zero.
|
||||
* The number of bytes actually read is returned as an integer.
|
||||
*
|
||||
* <p>
|
||||
* This method blocks until input data is available, end of file is
|
||||
* detected, or an exception is thrown.
|
||||
*
|
||||
* <p>
|
||||
* If <code>len</code> is zero, then no bytes are read and <code>0</code> is
|
||||
* returned; otherwise, there is an attempt to read at least one byte. If no
|
||||
* byte is available because the stream is at end of file, the value
|
||||
* <code>-1</code> is returned; otherwise, at least one byte is read and
|
||||
* stored into <code>b</code>.
|
||||
*
|
||||
* <p>
|
||||
* The first byte read is stored into element <code>b[off]</code>, the next
|
||||
* one into <code>b[off+1]</code>, and so on. The number of bytes read is,
|
||||
* at most, equal to <code>len</code>. Let <i>k</i> be the number of bytes
|
||||
* actually read; these bytes will be stored in elements <code>b[off]</code>
|
||||
* through <code>b[off+</code><i>k</i><code>-1]</code>, leaving elements
|
||||
* <code>b[off+</code><i>k</i><code>]</code> through
|
||||
* <code>b[off+len-1]</code> unaffected.
|
||||
*
|
||||
* <p>
|
||||
* In every case, elements <code>b[0]</code> through <code>b[off]</code> and
|
||||
* elements <code>b[off+len]</code> through <code>b[b.length-1]</code> are
|
||||
* unaffected.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @param off the start offset in the destination array <code>b</code>
|
||||
* @param len the maximum number of bytes read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> if there is no more data because the end of the
|
||||
* stream has been reached.
|
||||
* @throws NullPointerException If <code>b</code> is <code>null</code>.
|
||||
* @throws IndexOutOfBoundsException If <code>off</code> is negative, <code>len</code> is
|
||||
* negative, or <code>len</code> is greater than
|
||||
* <code>b.length - off</code>
|
||||
* @throws IOException if the first byte cannot be read for any reason other than
|
||||
* end of file, the stream has been closed and the underlying
|
||||
* input stream does not support reading after close, or
|
||||
* another I/O error occurs.
|
||||
* @see FilterInputStream#in
|
||||
* @see InputStream#read(byte[], int, int)
|
||||
*/
|
||||
public final int read(byte b[], int off, int len) throws IOException {
|
||||
return in.read(b, off, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readFully</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @throws EOFException if this input stream reaches the end before reading all
|
||||
* the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final void readFully(byte b[]) throws IOException {
|
||||
readFully(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readFully</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @param off the start offset of the data.
|
||||
* @param len the number of bytes to read.
|
||||
* @throws EOFException if this input stream reaches the end before reading all
|
||||
* the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final void readFully(byte b[], int off, int len) throws IOException {
|
||||
if (len < 0)
|
||||
throw new IndexOutOfBoundsException();
|
||||
int n = 0;
|
||||
while (n < len) {
|
||||
int count = in.read(b, off + n, len - n);
|
||||
if (count < 0)
|
||||
throw new EOFException();
|
||||
n += count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>skipBytes</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @param n the number of bytes to be skipped.
|
||||
* @return the actual number of bytes skipped.
|
||||
* @throws IOException if the contained input stream does not support seek, or
|
||||
* the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
*/
|
||||
public final int skipBytes(int n) throws IOException {
|
||||
int total = 0;
|
||||
int cur = 0;
|
||||
|
||||
while ((total < n) && ((cur = (int) in.skip(n - total)) > 0)) {
|
||||
total += cur;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readBoolean</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the <code>boolean</code> value read.
|
||||
* @throws EOFException if this input stream has reached the end.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final boolean readBoolean() throws IOException {
|
||||
int ch = in.read();
|
||||
if (ch < 0)
|
||||
throw new EOFException();
|
||||
return (ch != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readByte</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next byte of this input stream as a signed 8-bit
|
||||
* <code>byte</code>.
|
||||
* @throws EOFException if this input stream has reached the end.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final byte readByte() throws IOException {
|
||||
int ch = in.read();
|
||||
if (ch < 0)
|
||||
throw new EOFException();
|
||||
return (byte) (ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readUnsignedByte</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next byte of this input stream, interpreted as an unsigned
|
||||
* 8-bit number.
|
||||
* @throws EOFException if this input stream has reached the end.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final int readUnsignedByte() throws IOException {
|
||||
int ch = in.read();
|
||||
if (ch < 0)
|
||||
throw new EOFException();
|
||||
return ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readShort</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next two bytes of this input stream, interpreted as a signed
|
||||
* 16-bit number.
|
||||
* @throws EOFException if this input stream reaches the end before reading two
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final short readShort() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
if ((ch1 | ch2) < 0)
|
||||
throw new EOFException();
|
||||
return (short) ((ch1 << 0) + (ch2 << 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readUnsignedShort</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next two bytes of this input stream, interpreted as an
|
||||
* unsigned 16-bit integer.
|
||||
* @throws EOFException if this input stream reaches the end before reading two
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final int readUnsignedShort() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
if ((ch1 | ch2) < 0)
|
||||
throw new EOFException();
|
||||
return (ch1 << 0) + (ch2 << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readChar</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next two bytes of this input stream, interpreted as a
|
||||
* <code>char</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading two
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final char readChar() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
if ((ch1 | ch2) < 0)
|
||||
throw new EOFException();
|
||||
return (char) ((ch1 << 0) + (ch2 << 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readInt</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next four bytes of this input stream, interpreted as an
|
||||
* <code>int</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading four
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final int readInt() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
int ch3 = in.read();
|
||||
int ch4 = in.read();
|
||||
if ((ch1 | ch2 | ch3 | ch4) < 0)
|
||||
throw new EOFException();
|
||||
return ((ch1 << 0) + (ch2 << 8) + (ch3 << 16) + (ch4 << 24));
|
||||
}
|
||||
|
||||
private byte readBuffer[] = new byte[8];
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readLong</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next eight bytes of this input stream, interpreted as a
|
||||
* <code>long</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading eight
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final long readLong() throws IOException {
|
||||
readFully(readBuffer, 0, 8);
|
||||
return (((long) readBuffer[7] << 56)
|
||||
+ ((long) (readBuffer[6] & 255) << 48)
|
||||
+ ((long) (readBuffer[5] & 255) << 40)
|
||||
+ ((long) (readBuffer[4] & 255) << 32)
|
||||
+ ((long) (readBuffer[3] & 255) << 24)
|
||||
+ ((readBuffer[2] & 255) << 16) + ((readBuffer[1] & 255) << 8) + ((readBuffer[0] & 255) << 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readFloat</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next four bytes of this input stream, interpreted as a
|
||||
* <code>float</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading four
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see java.io.DataInputStream#readInt()
|
||||
* @see Float#intBitsToFloat(int)
|
||||
*/
|
||||
public final float readFloat() throws IOException {
|
||||
return Float.intBitsToFloat(readInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readDouble</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next eight bytes of this input stream, interpreted as a
|
||||
* <code>double</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading eight
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see java.io.DataInputStream#readLong()
|
||||
* @see Double#longBitsToDouble(long)
|
||||
*/
|
||||
public final double readDouble() throws IOException {
|
||||
return Double.longBitsToDouble(readLong());
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readUTF</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return a Unicode string.
|
||||
* @throws EOFException if this input stream reaches the end before reading all
|
||||
* the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
|
||||
* encoding of a string.
|
||||
* @see java.io.DataInputStream#readUTF(DataInput)
|
||||
*/
|
||||
public final String readUTF() throws IOException {
|
||||
return readUTF(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* working arrays initialized on demand by readUTF
|
||||
*/
|
||||
private byte bytearr[] = new byte[80];
|
||||
private char chararr[] = new char[80];
|
||||
|
||||
/**
|
||||
* Reads from the stream <code>in</code> a representation of a Unicode
|
||||
* character string encoded in <a
|
||||
* href="DataInput.html#modified-utf-8">modified UTF-8</a> format; this
|
||||
* string of characters is then returned as a <code>String</code>. The
|
||||
* details of the modified UTF-8 representation are exactly the same as for
|
||||
* the <code>readUTF</code> method of <code>DataInput</code>.
|
||||
*
|
||||
* @param in a data input stream.
|
||||
* @return a Unicode string.
|
||||
* @throws EOFException if the input stream reaches the end before all the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
|
||||
* encoding of a Unicode string.
|
||||
* @see java.io.DataInputStream#readUnsignedShort()
|
||||
*/
|
||||
public final static String readUTF(DataInput in) throws IOException {
|
||||
int utflen = in.readUnsignedShort();
|
||||
byte[] bytearr = null;
|
||||
char[] chararr = null;
|
||||
if (in instanceof LittleEndianDataInputStream) {
|
||||
LittleEndianDataInputStream dis = (LittleEndianDataInputStream) in;
|
||||
if (dis.bytearr.length < utflen) {
|
||||
dis.bytearr = new byte[utflen * 2];
|
||||
dis.chararr = new char[utflen * 2];
|
||||
}
|
||||
chararr = dis.chararr;
|
||||
bytearr = dis.bytearr;
|
||||
} else {
|
||||
bytearr = new byte[utflen];
|
||||
chararr = new char[utflen];
|
||||
}
|
||||
|
||||
int c, char2, char3;
|
||||
int count = 0;
|
||||
int chararr_count = 0;
|
||||
|
||||
in.readFully(bytearr, 0, utflen);
|
||||
|
||||
while (count < utflen) {
|
||||
c = (int) bytearr[count] & 0xff;
|
||||
if (c > 127)
|
||||
break;
|
||||
count++;
|
||||
chararr[chararr_count++] = (char) c;
|
||||
}
|
||||
|
||||
while (count < utflen) {
|
||||
c = (int) bytearr[count] & 0xff;
|
||||
switch (c >> 4) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
/* 0xxxxxxx */
|
||||
count++;
|
||||
chararr[chararr_count++] = (char) c;
|
||||
break;
|
||||
case 12:
|
||||
case 13:
|
||||
/* 110x xxxx 10xx xxxx */
|
||||
count += 2;
|
||||
if (count > utflen)
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input: partial character at end");
|
||||
char2 = (int) bytearr[count - 1];
|
||||
if ((char2 & 0xC0) != 0x80)
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input around byte " + count);
|
||||
chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F));
|
||||
break;
|
||||
case 14:
|
||||
/* 1110 xxxx 10xx xxxx 10xx xxxx */
|
||||
count += 3;
|
||||
if (count > utflen)
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input: partial character at end");
|
||||
char2 = (int) bytearr[count - 2];
|
||||
char3 = (int) bytearr[count - 1];
|
||||
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input around byte " + (count - 1));
|
||||
chararr[chararr_count++] = (char) (((c & 0x0F) << 12)
|
||||
| ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
|
||||
break;
|
||||
default:
|
||||
/* 10xx xxxx, 1111 xxxx */
|
||||
throw new UTFDataFormatException("malformed input around byte "
|
||||
+ count);
|
||||
}
|
||||
}
|
||||
// The number of chars produced may be less than utflen
|
||||
return new String(chararr, 0, chararr_count);
|
||||
}
|
||||
}
|
@ -0,0 +1,403 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import java.io.DataOutput;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UTFDataFormatException;
|
||||
|
||||
/**
|
||||
* A little endian java.io.DataOutputStream
|
||||
*/
|
||||
|
||||
public class LittleEndianDataOutputStream extends FilterOutputStream implements
|
||||
DataOutput {
|
||||
|
||||
/**
|
||||
* The number of bytes written to the data output stream so far. If this
|
||||
* counter overflows, it will be wrapped to Integer.MAX_VALUE.
|
||||
*/
|
||||
protected int written;
|
||||
|
||||
/**
|
||||
* Creates a new data output stream to write data to the specified
|
||||
* underlying output stream. The counter <code>written</code> is set to
|
||||
* zero.
|
||||
*
|
||||
* @param out the underlying output stream, to be saved for later use.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public LittleEndianDataOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the written counter by the specified value until it reaches
|
||||
* Integer.MAX_VALUE.
|
||||
*/
|
||||
private void incCount(int value) {
|
||||
int temp = written + value;
|
||||
if (temp < 0) {
|
||||
temp = Integer.MAX_VALUE;
|
||||
}
|
||||
written = temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the specified byte (the low eight bits of the argument
|
||||
* <code>b</code>) to the underlying output stream. If no exception is
|
||||
* thrown, the counter <code>written</code> is incremented by <code>1</code>
|
||||
* .
|
||||
* <p>
|
||||
* Implements the <code>write</code> method of <code>OutputStream</code>.
|
||||
*
|
||||
* @param b the <code>byte</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public synchronized void write(int b) throws IOException {
|
||||
out.write(b);
|
||||
incCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes <code>len</code> bytes from the specified byte array starting at
|
||||
* offset <code>off</code> to the underlying output stream. If no exception
|
||||
* is thrown, the counter <code>written</code> is incremented by
|
||||
* <code>len</code>.
|
||||
*
|
||||
* @param b the data.
|
||||
* @param off the start offset in the data.
|
||||
* @param len the number of bytes to write.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public synchronized void write(byte b[], int off, int len)
|
||||
throws IOException {
|
||||
out.write(b, off, len);
|
||||
incCount(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes this data output stream. This forces any buffered output bytes to
|
||||
* be written out to the stream.
|
||||
* <p>
|
||||
* The <code>flush</code> method of <code>DataOutputStream</code> calls the
|
||||
* <code>flush</code> method of its underlying output stream.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
* @see OutputStream#flush()
|
||||
*/
|
||||
public void flush() throws IOException {
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a <code>boolean</code> to the underlying output stream as a 1-byte
|
||||
* value. The value <code>true</code> is written out as the value
|
||||
* <code>(byte)1</code>; the value <code>false</code> is written out as the
|
||||
* value <code>(byte)0</code>. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>1</code>.
|
||||
*
|
||||
* @param v a <code>boolean</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeBoolean(boolean v) throws IOException {
|
||||
out.write(v ? 1 : 0);
|
||||
incCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out a <code>byte</code> to the underlying output stream as a
|
||||
* 1-byte value. If no exception is thrown, the counter <code>written</code>
|
||||
* is incremented by <code>1</code>.
|
||||
*
|
||||
* @param v a <code>byte</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeByte(int v) throws IOException {
|
||||
out.write(v);
|
||||
incCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a <code>short</code> to the underlying output stream as two bytes,
|
||||
* low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>2</code>.
|
||||
*
|
||||
* @param v a <code>short</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeShort(int v) throws IOException {
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
incCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a <code>char</code> to the underlying output stream as a 2-byte
|
||||
* value, low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>2</code>.
|
||||
*
|
||||
* @param v a <code>char</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeChar(int v) throws IOException {
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
incCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an <code>int</code> to the underlying output stream as four bytes,
|
||||
* low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>4</code>.
|
||||
*
|
||||
* @param v an <code>int</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeInt(int v) throws IOException {
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
out.write((v >>> 16) & 0xFF);
|
||||
out.write((v >>> 24) & 0xFF);
|
||||
incCount(4);
|
||||
}
|
||||
|
||||
private byte writeBuffer[] = new byte[8];
|
||||
|
||||
/**
|
||||
* Writes a <code>long</code> to the underlying output stream as eight
|
||||
* bytes, low byte first. In no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>8</code>.
|
||||
*
|
||||
* @param v a <code>long</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeLong(long v) throws IOException {
|
||||
writeBuffer[7] = (byte) (v >>> 56);
|
||||
writeBuffer[6] = (byte) (v >>> 48);
|
||||
writeBuffer[5] = (byte) (v >>> 40);
|
||||
writeBuffer[4] = (byte) (v >>> 32);
|
||||
writeBuffer[3] = (byte) (v >>> 24);
|
||||
writeBuffer[2] = (byte) (v >>> 16);
|
||||
writeBuffer[1] = (byte) (v >>> 8);
|
||||
writeBuffer[0] = (byte) (v >>> 0);
|
||||
out.write(writeBuffer, 0, 8);
|
||||
incCount(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the float argument to an <code>int</code> using the
|
||||
* <code>floatToIntBits</code> method in class <code>Float</code>, and then
|
||||
* writes that <code>int</code> value to the underlying output stream as a
|
||||
* 4-byte quantity, low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>4</code>.
|
||||
*
|
||||
* @param v a <code>float</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
* @see Float#floatToIntBits(float)
|
||||
*/
|
||||
public final void writeFloat(float v) throws IOException {
|
||||
writeInt(Float.floatToIntBits(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the double argument to a <code>long</code> using the
|
||||
* <code>doubleToLongBits</code> method in class <code>Double</code>, and
|
||||
* then writes that <code>long</code> value to the underlying output stream
|
||||
* as an 8-byte quantity, low byte first. If no exception is thrown, the
|
||||
* counter <code>written</code> is incremented by <code>8</code>.
|
||||
*
|
||||
* @param v a <code>double</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
* @see Double#doubleToLongBits(double)
|
||||
*/
|
||||
public final void writeDouble(double v) throws IOException {
|
||||
writeLong(Double.doubleToLongBits(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out the string to the underlying output stream as a sequence of
|
||||
* bytes. Each character in the string is written out, in sequence, by
|
||||
* discarding its high eight bits. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by the length of <code>s</code>.
|
||||
*
|
||||
* @param s a string of bytes to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeBytes(String s) throws IOException {
|
||||
int len = s.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
out.write((byte) s.charAt(i));
|
||||
}
|
||||
incCount(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the underlying output stream as a sequence of
|
||||
* characters. Each character is written to the data output stream as if by
|
||||
* the <code>writeChar</code> method. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by twice the length of <code>s</code>
|
||||
* .
|
||||
*
|
||||
* @param s a <code>String</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see java.io.DataOutputStream#writeChar(int)
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeChars(String s) throws IOException {
|
||||
int len = s.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
int v = s.charAt(i);
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
}
|
||||
incCount(len * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the underlying output stream using <a
|
||||
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
|
||||
* machine-independent manner.
|
||||
* <p>
|
||||
* First, two bytes are written to the output stream as if by the
|
||||
* <code>writeShort</code> method giving the number of bytes to follow. This
|
||||
* value is the number of bytes actually written out, not the length of the
|
||||
* string. Following the length, each character of the string is output, in
|
||||
* sequence, using the modified UTF-8 encoding for the character. If no
|
||||
* exception is thrown, the counter <code>written</code> is incremented by
|
||||
* the total number of bytes written to the output stream. This will be at
|
||||
* least two plus the length of <code>str</code>, and at most two plus
|
||||
* thrice the length of <code>str</code>.
|
||||
*
|
||||
* @param str a string to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public final void writeUTF(String str) throws IOException {
|
||||
writeUTF(str, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* bytearr is initialized on demand by writeUTF
|
||||
*/
|
||||
private byte[] bytearr = null;
|
||||
|
||||
/**
|
||||
* Writes a string to the specified DataOutput using <a
|
||||
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
|
||||
* machine-independent manner.
|
||||
* <p>
|
||||
* First, two bytes are written to out as if by the <code>writeShort</code>
|
||||
* method giving the number of bytes to follow. This value is the number of
|
||||
* bytes actually written out, not the length of the string. Following the
|
||||
* length, each character of the string is output, in sequence, using the
|
||||
* modified UTF-8 encoding for the character. If no exception is thrown, the
|
||||
* counter <code>written</code> is incremented by the total number of bytes
|
||||
* written to the output stream. This will be at least two plus the length
|
||||
* of <code>str</code>, and at most two plus thrice the length of
|
||||
* <code>str</code>.
|
||||
*
|
||||
* @param str a string to be written.
|
||||
* @param out destination to write to
|
||||
* @return The number of bytes written out.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
static int writeUTF(String str, DataOutput out) throws IOException {
|
||||
int strlen = str.length();
|
||||
int utflen = 0;
|
||||
int c, count = 0;
|
||||
|
||||
/* use charAt instead of copying String to char array */
|
||||
for (int i = 0; i < strlen; i++) {
|
||||
c = str.charAt(i);
|
||||
if ((c >= 0x0001) && (c <= 0x007F)) {
|
||||
utflen++;
|
||||
} else if (c > 0x07FF) {
|
||||
utflen += 3;
|
||||
} else {
|
||||
utflen += 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (utflen > 65535)
|
||||
throw new UTFDataFormatException("encoded string too long: "
|
||||
+ utflen + " bytes");
|
||||
|
||||
byte[] bytearr = null;
|
||||
if (out instanceof LittleEndianDataOutputStream) {
|
||||
LittleEndianDataOutputStream dos = (LittleEndianDataOutputStream) out;
|
||||
if (dos.bytearr == null || (dos.bytearr.length < (utflen + 2)))
|
||||
dos.bytearr = new byte[(utflen * 2) + 2];
|
||||
bytearr = dos.bytearr;
|
||||
} else {
|
||||
bytearr = new byte[utflen + 2];
|
||||
}
|
||||
|
||||
bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
|
||||
bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
|
||||
|
||||
int i = 0;
|
||||
for (i = 0; i < strlen; i++) {
|
||||
c = str.charAt(i);
|
||||
if (!((c >= 0x0001) && (c <= 0x007F)))
|
||||
break;
|
||||
bytearr[count++] = (byte) c;
|
||||
}
|
||||
|
||||
for (; i < strlen; i++) {
|
||||
c = str.charAt(i);
|
||||
if ((c >= 0x0001) && (c <= 0x007F)) {
|
||||
bytearr[count++] = (byte) c;
|
||||
|
||||
} else if (c > 0x07FF) {
|
||||
bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
|
||||
bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
|
||||
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
|
||||
} else {
|
||||
bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
|
||||
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
|
||||
}
|
||||
}
|
||||
out.write(bytearr, 0, utflen + 2);
|
||||
return utflen + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the counter <code>written</code>, the number
|
||||
* of bytes written to this data output stream so far. If the counter
|
||||
* overflows, it will be wrapped to Integer.MAX_VALUE.
|
||||
*
|
||||
* @return the value of the <code>written</code> field.
|
||||
* @see java.io.DataOutputStream#written
|
||||
*/
|
||||
public final int size() {
|
||||
return written;
|
||||
}
|
||||
}
|
@ -0,0 +1,550 @@
|
||||
/*
|
||||
* 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.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
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.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.m2049r.levin.scanner.Dispatcher;
|
||||
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.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.HashSet;
|
||||
import java.util.Iterator;
|
||||
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();
|
||||
|
||||
void setFavouriteNodes(Set<NodeInfo> favouriteNodes);
|
||||
}
|
||||
|
||||
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(new SwipeRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) {
|
||||
refresh();
|
||||
} 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");
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private AsyncFindNodes asyncFindNodes = null;
|
||||
|
||||
private void refresh() {
|
||||
if (asyncFindNodes != null) return; // ignore refresh request as one is ongoing
|
||||
asyncFindNodes = new AsyncFindNodes();
|
||||
updateRefreshElements();
|
||||
asyncFindNodes.execute();
|
||||
}
|
||||
|
||||
@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");
|
||||
EditDialog diag = createEditDialog(nodeItem);
|
||||
if (diag != null) {
|
||||
diag.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
switch (id) {
|
||||
case R.id.fab:
|
||||
EditDialog diag = createEditDialog(null);
|
||||
if (diag != null) {
|
||||
diag.show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncFindNodes extends AsyncTask<Void, NodeInfo, Boolean> {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
filterFavourites();
|
||||
nodesAdapter.setNodes(null);
|
||||
nodesAdapter.allowClick(false);
|
||||
tvPull.setText(getString(R.string.node_scanning));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Timber.d("scanning");
|
||||
Set<NodeInfo> seedList = new HashSet<>();
|
||||
seedList.addAll(nodeList);
|
||||
nodeList.clear();
|
||||
Timber.d("seed %d", seedList.size());
|
||||
Dispatcher d = new Dispatcher(new Dispatcher.Listener() {
|
||||
@Override
|
||||
public void onGet(NodeInfo 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);
|
||||
}
|
||||
});
|
||||
// also seed with monero seed nodes (see p2p/net_node.inl:410 in monero src)
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("107.152.130.98", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("212.83.175.67", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("5.9.100.248", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("163.172.182.165", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("161.67.132.39", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("198.74.231.92", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("195.154.123.123", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("212.83.172.165", 18080)));
|
||||
d.seedPeers(seedList);
|
||||
d.awaitTermination(NODES_TO_FIND);
|
||||
}
|
||||
// final (filtered) result
|
||||
nodeList.addAll(d.getRpcNodes());
|
||||
return true;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@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 undoChanges() {
|
||||
if (nodeBackup != null)
|
||||
nodeInfo.overwriteWith(nodeBackup);
|
||||
}
|
||||
|
||||
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 AlertDialog.Builder(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),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
undoChanges();
|
||||
closeDialog();
|
||||
nodesAdapter.dataSetChanged(); // 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
nodeInfo.clear();
|
||||
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.dataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Node {
|
||||
static public final String MAINNET = "mainnet";
|
||||
static public final String STAGENET = "stagenet";
|
||||
static public final String TESTNET = "testnet";
|
||||
|
||||
private String name = null;
|
||||
final private NetworkType networkType;
|
||||
InetAddress hostAddress;
|
||||
private String host;
|
||||
int rpcPort = 0;
|
||||
private int levinPort = 0;
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private boolean favourite = false;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hostAddress.hashCode();
|
||||
}
|
||||
|
||||
// Nodes are equal if they are the same host address & are on the same network
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof Node)) return false;
|
||||
final Node anotherNode = (Node) other;
|
||||
return (hostAddress.equals(anotherNode.hostAddress) && (networkType == anotherNode.networkType));
|
||||
}
|
||||
|
||||
static public Node fromString(String nodeString) {
|
||||
try {
|
||||
return new Node(nodeString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Timber.w(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Node(String nodeString) {
|
||||
if ((nodeString == null) || nodeString.isEmpty())
|
||||
throw new IllegalArgumentException("daemon is empty");
|
||||
String daemonAddress;
|
||||
String a[] = nodeString.split("@");
|
||||
if (a.length == 1) { // no credentials
|
||||
daemonAddress = a[0];
|
||||
username = "";
|
||||
password = "";
|
||||
} else if (a.length == 2) { // credentials
|
||||
String userPassword[] = a[0].split(":");
|
||||
if (userPassword.length != 2)
|
||||
throw new IllegalArgumentException("User:Password invalid");
|
||||
username = userPassword[0];
|
||||
if (!username.isEmpty()) {
|
||||
password = userPassword[1];
|
||||
} else {
|
||||
password = "";
|
||||
}
|
||||
daemonAddress = a[1];
|
||||
} else {
|
||||
throw new IllegalArgumentException("Too many @");
|
||||
}
|
||||
|
||||
String daParts[] = daemonAddress.split("/");
|
||||
if ((daParts.length > 3) || (daParts.length < 1))
|
||||
throw new IllegalArgumentException("Too many '/' or too few");
|
||||
|
||||
daemonAddress = daParts[0];
|
||||
String da[] = daemonAddress.split(":");
|
||||
if ((da.length > 2) || (da.length < 1))
|
||||
throw new IllegalArgumentException("Too many ':' or too few");
|
||||
String host = da[0];
|
||||
|
||||
if (daParts.length == 1) {
|
||||
networkType = NetworkType.NetworkType_Mainnet;
|
||||
} else {
|
||||
switch (daParts[1]) {
|
||||
case MAINNET:
|
||||
networkType = NetworkType.NetworkType_Mainnet;
|
||||
break;
|
||||
case STAGENET:
|
||||
networkType = NetworkType.NetworkType_Stagenet;
|
||||
break;
|
||||
case TESTNET:
|
||||
networkType = NetworkType.NetworkType_Testnet;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("invalid net: " + daParts[1]);
|
||||
}
|
||||
}
|
||||
if (networkType != WalletManager.getInstance().getNetworkType())
|
||||
throw new IllegalArgumentException("wrong net: " + networkType);
|
||||
|
||||
String name = host;
|
||||
if (daParts.length == 3) {
|
||||
try {
|
||||
name = URLDecoder.decode(daParts[2], "UTF-8");
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
Timber.w(ex); // if we can't encode it, we don't use it
|
||||
}
|
||||
}
|
||||
this.name = name;
|
||||
|
||||
int port;
|
||||
if (da.length == 2) {
|
||||
try {
|
||||
port = Integer.parseInt(da[1]);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Port not numeric");
|
||||
}
|
||||
} else {
|
||||
port = getDefaultRpcPort();
|
||||
}
|
||||
try {
|
||||
setHost(host);
|
||||
} catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException("cannot resolve host " + host);
|
||||
}
|
||||
this.rpcPort = port;
|
||||
this.levinPort = getDefaultLevinPort();
|
||||
}
|
||||
|
||||
public String toNodeString() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (!username.isEmpty() && !password.isEmpty()) {
|
||||
sb.append(username).append(":").append(password).append("@");
|
||||
}
|
||||
sb.append(host).append(":").append(rpcPort);
|
||||
sb.append("/");
|
||||
switch (networkType) {
|
||||
case NetworkType_Mainnet:
|
||||
sb.append(MAINNET);
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
sb.append(STAGENET);
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
sb.append(TESTNET);
|
||||
break;
|
||||
}
|
||||
if (name != null)
|
||||
try {
|
||||
sb.append("/").append(URLEncoder.encode(name, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
Timber.w(ex); // if we can't encode it, we don't store it
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Node() {
|
||||
this.networkType = WalletManager.getInstance().getNetworkType();
|
||||
}
|
||||
|
||||
// constructor used for created nodes from retrieved peer lists
|
||||
public Node(InetSocketAddress socketAddress) {
|
||||
this();
|
||||
this.hostAddress = socketAddress.getAddress();
|
||||
this.host = socketAddress.getHostString();
|
||||
this.rpcPort = 0; // unknown
|
||||
this.levinPort = socketAddress.getPort();
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
//this.name = socketAddress.getHostName(); // triggers DNS so we don't do it by default
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return getHostAddress() + ":" + rpcPort;
|
||||
}
|
||||
|
||||
public String getHostAddress() {
|
||||
return hostAddress.getHostAddress();
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getRpcPort() {
|
||||
return rpcPort;
|
||||
}
|
||||
|
||||
public void setHost(String host) throws UnknownHostException {
|
||||
if ((host == null) || (host.isEmpty()))
|
||||
throw new UnknownHostException("loopback not supported (yet?)");
|
||||
this.host = host;
|
||||
this.hostAddress = InetAddress.getByName(host);
|
||||
}
|
||||
|
||||
public void setUsername(String user) {
|
||||
username = user;
|
||||
}
|
||||
|
||||
public void setPassword(String pass) {
|
||||
password = pass;
|
||||
}
|
||||
|
||||
public void setRpcPort(int port) {
|
||||
this.rpcPort = port;
|
||||
}
|
||||
|
||||
public void setName() {
|
||||
if (name == null)
|
||||
this.name = hostAddress.getHostName();
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if ((name == null) || (name.isEmpty()))
|
||||
this.name = hostAddress.getHostName();
|
||||
else
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public NetworkType getNetworkType() {
|
||||
return networkType;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public boolean isFavourite() {
|
||||
return favourite;
|
||||
}
|
||||
|
||||
public void setFavourite(boolean favourite) {
|
||||
this.favourite = favourite;
|
||||
}
|
||||
|
||||
public void toggleFavourite() {
|
||||
favourite = !favourite;
|
||||
}
|
||||
|
||||
public Node(Node anotherNode) {
|
||||
networkType = anotherNode.networkType;
|
||||
overwriteWith(anotherNode);
|
||||
}
|
||||
|
||||
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;
|
||||
username = anotherNode.username;
|
||||
password = anotherNode.password;
|
||||
favourite = anotherNode.favourite;
|
||||
}
|
||||
|
||||
static private int DEFAULT_LEVIN_PORT = 0;
|
||||
|
||||
// every node knows its network, but they are all the same
|
||||
static public int getDefaultLevinPort() {
|
||||
if (DEFAULT_LEVIN_PORT > 0) return DEFAULT_LEVIN_PORT;
|
||||
switch (WalletManager.getInstance().getNetworkType()) {
|
||||
case NetworkType_Mainnet:
|
||||
DEFAULT_LEVIN_PORT = 18080;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
DEFAULT_LEVIN_PORT = 28080;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
DEFAULT_LEVIN_PORT = 38080;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
|
||||
}
|
||||
return DEFAULT_LEVIN_PORT;
|
||||
}
|
||||
|
||||
static private int DEFAULT_RPC_PORT = 0;
|
||||
|
||||
// every node knows its network, but they are all the same
|
||||
static public int getDefaultRpcPort() {
|
||||
if (DEFAULT_RPC_PORT > 0) return DEFAULT_RPC_PORT;
|
||||
switch (WalletManager.getInstance().getNetworkType()) {
|
||||
case NetworkType_Mainnet:
|
||||
DEFAULT_RPC_PORT = 18081;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
DEFAULT_RPC_PORT = 28081;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
DEFAULT_RPC_PORT = 38081;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
|
||||
}
|
||||
return DEFAULT_RPC_PORT;
|
||||
}
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
||||
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
||||
import com.burgstaller.okhttp.digest.Credentials;
|
||||
import com.burgstaller.okhttp.digest.DigestAuthenticator;
|
||||
import com.m2049r.levin.scanner.Dispatcher;
|
||||
import com.m2049r.xmrwallet.util.OkHttpHelper;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class NodeInfo extends Node {
|
||||
final static public int MIN_MAJOR_VERSION = 9;
|
||||
|
||||
private long height = 0;
|
||||
private long timestamp = 0;
|
||||
private int majorVersion = 0;
|
||||
private double responseTime = Double.MAX_VALUE;
|
||||
private int responseCode = 0;
|
||||
|
||||
public void clear() {
|
||||
height = 0;
|
||||
majorVersion = 0;
|
||||
responseTime = Double.MAX_VALUE;
|
||||
responseCode = 0;
|
||||
timestamp = 0;
|
||||
}
|
||||
|
||||
static public NodeInfo fromString(String nodeString) {
|
||||
try {
|
||||
return new NodeInfo(nodeString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Timber.w(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public NodeInfo(NodeInfo anotherNode) {
|
||||
super(anotherNode);
|
||||
overwriteWith(anotherNode);
|
||||
}
|
||||
|
||||
private SocketAddress levinSocketAddress = null;
|
||||
|
||||
synchronized public SocketAddress getLevinSocketAddress() {
|
||||
if (levinSocketAddress == null) {
|
||||
// use default peer port if not set - very few peers use nonstandard port
|
||||
levinSocketAddress = new InetSocketAddress(hostAddress, getDefaultLevinPort());
|
||||
}
|
||||
return levinSocketAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return super.equals(other);
|
||||
}
|
||||
|
||||
public NodeInfo(String nodeString) {
|
||||
super(nodeString);
|
||||
}
|
||||
|
||||
public NodeInfo(InetSocketAddress socketAddress) {
|
||||
super(socketAddress);
|
||||
}
|
||||
|
||||
public NodeInfo() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NodeInfo(InetSocketAddress peerAddress, long height, int majorVersion, double respTime) {
|
||||
super(peerAddress);
|
||||
this.height = height;
|
||||
this.majorVersion = majorVersion;
|
||||
this.responseTime = respTime;
|
||||
}
|
||||
|
||||
public long getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public int getMajorVersion() {
|
||||
return majorVersion;
|
||||
}
|
||||
|
||||
public double getResponseTime() {
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public int getResponseCode() {
|
||||
return responseCode;
|
||||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
return (responseCode >= 200) && (responseCode < 300);
|
||||
}
|
||||
|
||||
public boolean isUnauthorized() {
|
||||
return responseCode == HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return isSuccessful() && (majorVersion >= MIN_MAJOR_VERSION) && (responseTime < Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
static public Comparator<NodeInfo> BestNodeComparator = new Comparator<NodeInfo>() {
|
||||
@Override
|
||||
public int compare(NodeInfo o1, NodeInfo o2) {
|
||||
if (o1.isValid()) {
|
||||
if (o2.isValid()) { // both are valid
|
||||
// higher node wins
|
||||
int heightDiff = (int) (o2.height - o1.height);
|
||||
if (Math.abs(heightDiff) > Dispatcher.HEIGHT_WINDOW)
|
||||
return heightDiff;
|
||||
// if they are (nearly) equal, faster node wins
|
||||
return (int) Math.signum(o1.responseTime - o2.responseTime);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void overwriteWith(NodeInfo anotherNode) {
|
||||
super.overwriteWith(anotherNode);
|
||||
height = anotherNode.height;
|
||||
timestamp = anotherNode.timestamp;
|
||||
majorVersion = anotherNode.majorVersion;
|
||||
responseTime = anotherNode.responseTime;
|
||||
responseCode = anotherNode.responseCode;
|
||||
}
|
||||
|
||||
public String toNodeString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(super.toString());
|
||||
sb.append("?rc=").append(responseCode);
|
||||
sb.append("?v=").append(majorVersion);
|
||||
sb.append("&h=").append(height);
|
||||
sb.append("&ts=").append(timestamp);
|
||||
if (responseTime < Double.MAX_VALUE) {
|
||||
sb.append("&t=").append(responseTime).append("ms");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final int HTTP_TIMEOUT = OkHttpHelper.HTTP_TIMEOUT;
|
||||
public static final double PING_GOOD = HTTP_TIMEOUT / 3; //ms
|
||||
public static final double PING_MEDIUM = 2 * PING_GOOD; //ms
|
||||
public static final double PING_BAD = HTTP_TIMEOUT;
|
||||
|
||||
public boolean testRpcService() {
|
||||
return testRpcService(rpcPort);
|
||||
}
|
||||
|
||||
private boolean testRpcService(int port) {
|
||||
clear();
|
||||
try {
|
||||
OkHttpClient client = OkHttpHelper.getEagerClient();
|
||||
if (!getUsername().isEmpty()) {
|
||||
final DigestAuthenticator authenticator =
|
||||
new DigestAuthenticator(new Credentials(getUsername(), getPassword()));
|
||||
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
|
||||
client = client.newBuilder()
|
||||
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
|
||||
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
|
||||
.build();
|
||||
}
|
||||
HttpUrl url = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(getHostAddress())
|
||||
.port(port)
|
||||
.addPathSegment("json_rpc")
|
||||
.build();
|
||||
final RequestBody reqBody = RequestBody
|
||||
.create(MediaType.parse("application/json"),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}");
|
||||
Request request = OkHttpHelper.getPostRequest(url, reqBody);
|
||||
long ta = System.nanoTime();
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
responseTime = (System.nanoTime() - ta) / 1000000.0;
|
||||
responseCode = response.code();
|
||||
if (response.isSuccessful()) {
|
||||
ResponseBody respBody = response.body(); // closed through Response object
|
||||
if ((respBody != null) && (respBody.contentLength() < 1000)) { // sanity check
|
||||
final JSONObject json = new JSONObject(
|
||||
respBody.string());
|
||||
final JSONObject header = json.getJSONObject(
|
||||
"result").getJSONObject("block_header");
|
||||
height = header.getLong("height");
|
||||
timestamp = header.getLong("timestamp");
|
||||
majorVersion = header.getInt("major_version");
|
||||
return true; // success
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | JSONException ex) {
|
||||
// failure
|
||||
Timber.d(ex.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static final private int[] TEST_PORTS = {18089}; // check only opt-in port
|
||||
|
||||
public boolean findRpcService() {
|
||||
// if already have an rpcPort, use that
|
||||
if (rpcPort > 0) return testRpcService(rpcPort);
|
||||
// otherwise try to find one
|
||||
for (int port : TEST_PORTS) {
|
||||
if (testRpcService(port)) { // found a service
|
||||
this.rpcPort = port;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
public class WalletNode {
|
||||
private final String name;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String user;
|
||||
private final String password;
|
||||
private final NetworkType networkType;
|
||||
|
||||
public WalletNode(String walletName, String daemon, NetworkType networkType) {
|
||||
if ((daemon == null) || daemon.isEmpty())
|
||||
throw new IllegalArgumentException("daemon is empty");
|
||||
this.name = walletName;
|
||||
String daemonAddress;
|
||||
String a[] = daemon.split("@");
|
||||
if (a.length == 1) { // no credentials
|
||||
daemonAddress = a[0];
|
||||
user = "";
|
||||
password = "";
|
||||
} else if (a.length == 2) { // credentials
|
||||
String userPassword[] = a[0].split(":");
|
||||
if (userPassword.length != 2)
|
||||
throw new IllegalArgumentException("User:Password invalid");
|
||||
user = userPassword[0];
|
||||
if (!user.isEmpty()) {
|
||||
password = userPassword[1];
|
||||
} else {
|
||||
password = "";
|
||||
}
|
||||
daemonAddress = a[1];
|
||||
} else {
|
||||
throw new IllegalArgumentException("Too many @");
|
||||
}
|
||||
|
||||
String da[] = daemonAddress.split(":");
|
||||
if ((da.length > 2) || (da.length < 1))
|
||||
throw new IllegalArgumentException("Too many ':' or too few");
|
||||
host = da[0];
|
||||
if (da.length == 2) {
|
||||
try {
|
||||
port = Integer.parseInt(da[1]);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Port not numeric");
|
||||
}
|
||||
} else {
|
||||
switch (networkType) {
|
||||
case NetworkType_Mainnet:
|
||||
port = 18081;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
port = 28081;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
port = 38081;
|
||||
break;
|
||||
default:
|
||||
port = 0;
|
||||
}
|
||||
}
|
||||
this.networkType = networkType;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public NetworkType getNetworkType() {
|
||||
return networkType;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return host + ":" + port;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public SocketAddress getSocketAddress() {
|
||||
return new InetSocketAddress(host, port);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return !host.isEmpty();
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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.layout;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHolder> {
|
||||
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
public interface OnInteractionListener {
|
||||
void onInteraction(View view, NodeInfo item);
|
||||
}
|
||||
|
||||
private final List<NodeInfo> nodeItems = new ArrayList<>();
|
||||
private final OnInteractionListener listener;
|
||||
|
||||
private Context context;
|
||||
|
||||
public NodeInfoAdapter(Context context, OnInteractionListener listener) {
|
||||
this.context = context;
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
TS_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_node, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final @NonNull ViewHolder holder, int position) {
|
||||
holder.bind(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return nodeItems.size();
|
||||
}
|
||||
|
||||
public void addNode(NodeInfo node) {
|
||||
if (!nodeItems.contains(node))
|
||||
nodeItems.add(node);
|
||||
dataSetChanged(); // in case the nodeinfo has changed
|
||||
}
|
||||
|
||||
public void dataSetChanged() {
|
||||
Collections.sort(nodeItems, NodeInfo.BestNodeComparator);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setNodes(Collection<NodeInfo> data) {
|
||||
nodeItems.clear();
|
||||
if (data != null) {
|
||||
for (NodeInfo node : data) {
|
||||
if (!nodeItems.contains(node))
|
||||
nodeItems.add(node);
|
||||
}
|
||||
}
|
||||
dataSetChanged();
|
||||
}
|
||||
|
||||
private boolean itemsClickable = true;
|
||||
|
||||
public void allowClick(boolean clickable) {
|
||||
itemsClickable = clickable;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final ImageButton ibBookmark;
|
||||
final TextView tvName;
|
||||
final TextView tvIp;
|
||||
final ImageView ivPing;
|
||||
NodeInfo nodeItem;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ibBookmark = itemView.findViewById(R.id.ibBookmark);
|
||||
tvName = itemView.findViewById(R.id.tvName);
|
||||
tvIp = itemView.findViewById(R.id.tvAddress);
|
||||
ivPing = itemView.findViewById(R.id.ivPing);
|
||||
ibBookmark.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
nodeItem.toggleFavourite();
|
||||
showStar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showStar() {
|
||||
if (nodeItem.isFavourite()) {
|
||||
ibBookmark.setImageResource(R.drawable.ic_bookmark_24dp);
|
||||
} else {
|
||||
ibBookmark.setImageResource(R.drawable.ic_bookmark_border_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
void bind(int position) {
|
||||
nodeItem = nodeItems.get(position);
|
||||
tvName.setText(nodeItem.getName());
|
||||
final String ts = TS_FORMATTER.format(new Date(nodeItem.getTimestamp() * 1000));
|
||||
ivPing.setImageResource(getPingIcon(nodeItem));
|
||||
if (nodeItem.isValid()) {
|
||||
tvIp.setText(context.getString(R.string.node_height, ts));
|
||||
} else {
|
||||
tvIp.setText(getResponseErrorText(context, nodeItem.getResponseCode()));
|
||||
}
|
||||
itemView.setOnClickListener(this);
|
||||
itemView.setClickable(itemsClickable);
|
||||
ibBookmark.setClickable(itemsClickable);
|
||||
showStar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // gets item position
|
||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||
listener.onInteraction(view, nodeItems.get(position));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static public int getPingIcon(NodeInfo nodeInfo) {
|
||||
if (nodeInfo.isUnauthorized()) {
|
||||
return R.drawable.ic_wifi_lock_black_24dp;
|
||||
}
|
||||
if (nodeInfo.isValid()) {
|
||||
final double ping = nodeInfo.getResponseTime();
|
||||
if (ping < NodeInfo.PING_GOOD) {
|
||||
return R.drawable.ic_signal_wifi_4_bar_black_24dp;
|
||||
} else if (ping < NodeInfo.PING_MEDIUM) {
|
||||
return R.drawable.ic_signal_wifi_3_bar_black_24dp;
|
||||
} else if (ping < NodeInfo.PING_BAD) {
|
||||
return R.drawable.ic_signal_wifi_2_bar_black_24dp;
|
||||
} else {
|
||||
return R.drawable.ic_signal_wifi_1_bar_black_24dp;
|
||||
}
|
||||
} else {
|
||||
return R.drawable.ic_signal_wifi_off_black_24dp;
|
||||
}
|
||||
}
|
||||
|
||||
static public String getResponseErrorText(Context ctx, int responseCode) {
|
||||
if (responseCode == 0) {
|
||||
return ctx.getResources().getString(R.string.node_general_error);
|
||||
} else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
return ctx.getResources().getString(R.string.node_auth_error);
|
||||
} else {
|
||||
return ctx.getResources().getString(R.string.node_test_error, responseCode);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class NodeList {
|
||||
private static final int MAX_SIZE = 5;
|
||||
|
||||
private List<String> nodes = new ArrayList<>();
|
||||
|
||||
public List<String> getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void setRecent(String aNode) {
|
||||
if (aNode.trim().isEmpty()) return;
|
||||
boolean found = false;
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes.get(i).equals(aNode)) { // node is already in the list => move it to top
|
||||
nodes.remove(i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (nodes.size() > MAX_SIZE) {
|
||||
nodes.remove(nodes.size() - 1); // drop last one
|
||||
}
|
||||
}
|
||||
nodes.add(0, aNode);
|
||||
}
|
||||
|
||||
public NodeList(String aString) {
|
||||
String[] newNodes = aString.split(";");
|
||||
nodes.addAll(Arrays.asList(newNodes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (String node : this.nodes) {
|
||||
sb.append(node).append(";");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class OkHttpClientSingleton {
|
||||
static private OkHttpClient Singleton;
|
||||
|
||||
static public final OkHttpClient getOkHttpClient() {
|
||||
if (Singleton == null) {
|
||||
synchronized (com.m2049r.xmrwallet.util.OkHttpClientSingleton.class) {
|
||||
if (Singleton == null) {
|
||||
Singleton = new OkHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Singleton;
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
public class OkHttpHelper {
|
||||
static private OkHttpClient Singleton;
|
||||
|
||||
static public OkHttpClient getOkHttpClient() {
|
||||
if (Singleton == null) {
|
||||
synchronized (OkHttpHelper.class) {
|
||||
if (Singleton == null) {
|
||||
Singleton = new OkHttpClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Singleton;
|
||||
}
|
||||
|
||||
public static final int HTTP_TIMEOUT = 1000; //ms
|
||||
|
||||
static private OkHttpClient EagerSingleton;
|
||||
|
||||
static public OkHttpClient getEagerClient() {
|
||||
if (EagerSingleton == null) {
|
||||
synchronized (OkHttpHelper.class) {
|
||||
if (EagerSingleton == null) {
|
||||
EagerSingleton = new OkHttpClient.Builder()
|
||||
.connectTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return EagerSingleton;
|
||||
}
|
||||
|
||||
static final public String USER_AGENT = "Monerujo/1.0";
|
||||
|
||||
static public Request getPostRequest(HttpUrl url, RequestBody requestBody) {
|
||||
return new Request.Builder().url(url).post(requestBody)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
}
|
||||
|
||||
static public Request getGetRequest(HttpUrl url) {
|
||||
return new Request.Builder().url(url).get()
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="45"
|
||||
android:endColor="@color/moneroStreetA"
|
||||
android:startColor="@color/moneroStreetB"
|
||||
android:type="linear" />
|
||||
<corners android:radius="56dp" />
|
||||
</shape>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/gradientPink"
|
||||
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/gradientPink"
|
||||
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
@ -1,33 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="230.0"
|
||||
android:viewportWidth="230.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M0 0L0 230L230 230L230 0L0 0z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10 10L10 80L80 80L80 10L10 10M110 10L110 20L120 20L120 10L110 10M130 10L130 30L140 30L140 10L130 10M150 10L150 80L220 80L220 10L150 10z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M20 20L20 70L70 70L70 20L20 20M160 20L160 70L210 70L210 20L160 20z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M30 30L30 60L60 60L60 30L30 30M90 30L90 40L100 40L100 30L90 30M110 30L110 40L120 40L120 30L110 30M170 30L170 60L200 60L200 30L170 30M130 40L130 50L120 50L120 60L110 60L110 50L100 50L100 70L90 70L90 90L50 90L50 120L60 120L60 130L50 130L50 140L60 140L60 130L70 130L70 140L90 140L90 180L100 180L100 170L110 170L110 180L120 180L120 190L90 190L90 220L100 220L100 210L110 210L110 220L130 220L130 210L140 210L140 220L160 220L160 210L170 210L170 220L190 220L190 210L180 210L180 200L200 200L200 220L220 220L220 210L210 210L210 200L220 200L220 190L210 190L210 180L200 180L200 190L180 190L180 170L160 170L160 160L180 160L180 140L160 140L160 130L180 130L180 120L190 120L190 110L200 110L200 120L220 120L220 100L200 100L200 90L190 90L190 110L180 110L180 100L160 100L160 90L130 90L130 100L120 100L120 70L130 70L130 80L140 80L140 70L130 70L130 60L140 60L140 40L130 40z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M100 70L100 100L90 100L90 110L80 110L80 100L70 100L70 110L60 110L60 120L70 120L70 130L90 130L90 140L100 140L100 150L110 150L110 140L120 140L120 160L110 160L110 170L120 170L120 180L130 180L130 200L140 200L140 180L150 180L150 190L170 190L170 180L150 180L150 160L160 160L160 150L150 150L150 130L160 130L160 110L150 110L150 100L130 100L130 120L140 120L140 110L150 110L150 130L130 130L130 140L120 140L120 120L110 120L110 130L100 130L100 120L90 120L90 110L100 110L100 100L110 100L110 110L120 110L120 100L110 100L110 70L100 70z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10 90L10 120L30 120L30 130L10 130L10 140L30 140L30 130L40 130L40 120L30 120L30 110L20 110L20 100L40 100L40 90L10 90M70 110L70 120L80 120L80 110L70 110M200 130L200 140L190 140L190 150L200 150L200 170L210 170L210 160L220 160L220 150L210 150L210 130L200 130M10 150L10 220L80 220L80 150L10 150M130 150L130 160L140 160L140 150L130 150z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M20 160L20 210L70 210L70 160L20 160z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M30 170L30 200L60 200L60 170L30 170M130 170L130 180L140 180L140 170L130 170z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M200 190L200 200L210 200L210 190L200 190M110 200L110 210L120 210L120 200L110 200M150 200L150 210L160 210L160 200L150 200z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/moneroBlack"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillAlpha=".3"
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.01,21.49L23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4C5.28,3 0.81,6.66 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.67,14.86L12,21.49v0.01l0.01,-0.01 5.33,-6.63C17.06,14.65 15.03,13 12,13s-5.06,1.65 -5.33,1.86z" />
|
||||
</vector>
|
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillAlpha=".3"
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.01,21.49L23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4C5.28,3 0.81,6.66 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.79,12.52l7.2,8.98H12l0.01,-0.01 7.2,-8.98C18.85,12.24 16.1,10 12,10s-6.85,2.24 -7.21,2.52z" />
|
||||
</vector>
|
@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillAlpha=".3"
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.01,21.49L23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4C5.28,3 0.81,6.66 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3.53,10.95l8.46,10.54 0.01,0.01 0.01,-0.01 8.46,-10.54C20.04,10.62 16.81,8 12,8c-4.81,0 -8.04,2.62 -8.47,2.95z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.01,21.49L23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4C5.28,3 0.81,6.66 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z" />
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20.5,9.5c0.28,0 0.55,0.04 0.81,0.08L24,6c-3.34,-2.51 -7.5,-4 -12,-4S3.34,3.49 0,6l12,16 3.5,-4.67L15.5,14.5c0,-2.76 2.24,-5 5,-5zM23,16v-1.5c0,-1.38 -1.12,-2.5 -2.5,-2.5S18,13.12 18,14.5L18,16c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1zM22,16h-3v-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5L22,16z" />
|
||||
</vector>
|
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout 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:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llNotice"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPull"
|
||||
android:layout_below="@+id/llNotice"
|
||||
style="@style/MoneroLabel.Heading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/node_pull_hint" />
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/pullToRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/tvPull">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="72dp"
|
||||
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_node" />
|
||||
</LinearLayout>
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="24dp"
|
||||
android:background="@drawable/gradient_street_efab"
|
||||
android:elevation="6dp">
|
||||
|
||||
<android.support.design.button.MaterialButton
|
||||
android:id="@+id/fab"
|
||||
style="@style/ExtendedFAB"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
android:backgroundTintMode="src_in"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/node_fab_add"
|
||||
app:cornerRadius="56dp"
|
||||
app:icon="@drawable/ic_add_white_24dp" />
|
||||
</FrameLayout>
|
||||
</RelativeLayout>
|
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/selector_login">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/ibBookmark"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:src="@drawable/ic_bookmark_border_24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llNode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_toStartOf="@+id/ivPing"
|
||||
android:layout_toEndOf="@id/ibBookmark"
|
||||
android:gravity="start"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvName"
|
||||
style="@style/MoneroText.PosAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="monero-v9.monerujo.io" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAddress"
|
||||
style="@style/MoneroText.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="128.130.233.151:18089" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivPing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_signal_wifi_4_bar_black_24dp" />
|
||||
</RelativeLayout>
|
@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/header_top"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="10">
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/etNodeHost"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="8"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
style="@style/MoneroEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/node_address_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="textStart"
|
||||
tools:text="node.supportxmr.com" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/etNodePort"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
style="@style/MoneroEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/node_port_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="textStart"
|
||||
tools:text="18089" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/etNodeName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/data_top">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
style="@style/MoneroEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/node_name_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="textStart"
|
||||
tools:text="supportxmr" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/data_top"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/etNodeUser"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
style="@style/MoneroEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/node_user_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="textStart"
|
||||
tools:text="userxyz" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/etNodePass"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
style="@style/MoneroEdit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/node_pass_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="textStart"
|
||||
tools:text="secret" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/header_top_first">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvResultLabel"
|
||||
style="@style/MoneroLabel.Heading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@string/node_result_label"
|
||||
android:textAlignment="textStart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvResult"
|
||||
style="@style/MoneroText.Medium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_alignBaseline="@+id/tvResultLabel"
|
||||
android:layout_toEndOf="@+id/tvResultLabel"
|
||||
android:textAlignment="textStart"
|
||||
tools:text="Blockheight: 1710998 (v9), Ping: 187ms" />
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_help_node"
|
||||
android:icon="@drawable/ic_help_white_24dp"
|
||||
android:orderInCategory="500"
|
||||
android:title="@string/menu_help"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue